mirror of
https://github.com/telekom-security/tpotce.git
synced 2025-04-19 21:52:27 +00:00
1341 lines
31 KiB
C
1341 lines
31 KiB
C
/*
|
|
p0f - TCP/IP packet matching
|
|
----------------------------
|
|
|
|
Copyright (C) 2012 by Michal Zalewski <lcamtuf@coredump.cx>
|
|
|
|
Distributed under the terms and conditions of GNU LGPL.
|
|
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
|
|
#include <netinet/in.h>
|
|
#include <sys/types.h>
|
|
#include <ctype.h>
|
|
|
|
#include "types.h"
|
|
#include "config.h"
|
|
#include "debug.h"
|
|
#include "alloc-inl.h"
|
|
#include "process.h"
|
|
#include "hash.h"
|
|
#include "tcp.h"
|
|
#include "readfp.h"
|
|
#include "p0f.h"
|
|
|
|
#include "fp_tcp.h"
|
|
|
|
/* TCP signature buckets: */
|
|
|
|
static struct tcp_sig_record* sigs[2][SIG_BUCKETS];
|
|
static u32 sig_cnt[2][SIG_BUCKETS];
|
|
|
|
|
|
/* Figure out what the TTL distance might have been for an unknown sig. */
|
|
|
|
static u8 guess_dist(u8 ttl) {
|
|
if (ttl <= 32) return 32 - ttl;
|
|
if (ttl <= 64) return 64 - ttl;
|
|
if (ttl <= 128) return 128 - ttl;
|
|
return 255 - ttl;
|
|
}
|
|
|
|
|
|
/* Figure out if window size is a multiplier of MSS or MTU. We don't take window
|
|
scaling into account, because neither do TCP stack developers. */
|
|
|
|
static s16 detect_win_multi(struct tcp_sig* ts, u8* use_mtu, u16 syn_mss) {
|
|
|
|
u16 win = ts->win;
|
|
s32 mss = ts->mss, mss12 = mss - 12;
|
|
|
|
if (!win || mss < 100 || ts->win_type != WIN_TYPE_NORMAL)
|
|
return -1;
|
|
|
|
#define RET_IF_DIV(_div, _use_mtu, _desc) do { \
|
|
if ((_div) && !(win % (_div))) { \
|
|
*use_mtu = (_use_mtu); \
|
|
DEBUG("[#] Window size %u is a multiple of %s [%u].\n", win, _desc, _div); \
|
|
return win / (_div); \
|
|
} \
|
|
} while (0)
|
|
|
|
RET_IF_DIV(mss, 0, "MSS");
|
|
|
|
/* Some systems will sometimes subtract 12 bytes when timestamps are in use. */
|
|
|
|
if (ts->ts1) RET_IF_DIV(mss12, 0, "MSS - 12");
|
|
|
|
/* Some systems use MTU on the wrong interface, so let's check for the most
|
|
common case. */
|
|
|
|
RET_IF_DIV(1500 - MIN_TCP4, 0, "MSS (MTU = 1500, IPv4)");
|
|
RET_IF_DIV(1500 - MIN_TCP4 - 12, 0, "MSS (MTU = 1500, IPv4 - 12)");
|
|
|
|
if (ts->ip_ver == IP_VER6) {
|
|
|
|
RET_IF_DIV(1500 - MIN_TCP6, 0, "MSS (MTU = 1500, IPv6)");
|
|
RET_IF_DIV(1500 - MIN_TCP6 - 12, 0, "MSS (MTU = 1500, IPv6 - 12)");
|
|
|
|
}
|
|
|
|
/* Some systems use MTU instead of MSS: */
|
|
|
|
RET_IF_DIV(mss + MIN_TCP4, 1, "MTU (IPv4)");
|
|
RET_IF_DIV(mss + ts->tot_hdr, 1, "MTU (actual size)");
|
|
if (ts->ip_ver == IP_VER6) RET_IF_DIV(mss + MIN_TCP6, 1, "MTU (IPv6)");
|
|
RET_IF_DIV(1500, 1, "MTU (1500)");
|
|
|
|
/* On SYN+ACKs, some systems use of the peer: */
|
|
|
|
if (syn_mss) {
|
|
|
|
RET_IF_DIV(syn_mss, 0, "peer MSS");
|
|
RET_IF_DIV(syn_mss - 12, 0, "peer MSS - 12");
|
|
|
|
}
|
|
|
|
#undef RET_IF_DIV
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* See if any of the p0f.fp signatures matches the collected data. */
|
|
|
|
static void tcp_find_match(u8 to_srv, struct tcp_sig* ts, u8 dupe_det,
|
|
u16 syn_mss) {
|
|
|
|
struct tcp_sig_record* fmatch = NULL;
|
|
struct tcp_sig_record* gmatch = NULL;
|
|
|
|
u32 bucket = ts->opt_hash % SIG_BUCKETS;
|
|
u32 i;
|
|
|
|
u8 use_mtu = 0;
|
|
s16 win_multi = detect_win_multi(ts, &use_mtu, syn_mss);
|
|
|
|
CP(sigs[to_srv][bucket]);
|
|
|
|
for (i = 0; i < sig_cnt[to_srv][bucket]; i++) {
|
|
|
|
struct tcp_sig_record* ref = sigs[to_srv][bucket] + i;
|
|
struct tcp_sig* refs = CP(ref->sig);
|
|
|
|
u8 fuzzy = 0;
|
|
u32 ref_quirks = refs->quirks;
|
|
|
|
if (ref->sig->opt_hash != ts->opt_hash) continue;
|
|
|
|
/* If the p0f.fp signature has no IP version specified, we need
|
|
to remove IPv6-specific quirks from it when matching IPv4
|
|
packets, and vice versa. */
|
|
|
|
if (refs->ip_ver == -1)
|
|
ref_quirks &= ((ts->ip_ver == IP_VER4) ? ~(QUIRK_FLOW) :
|
|
~(QUIRK_DF | QUIRK_NZ_ID | QUIRK_ZERO_ID));
|
|
|
|
if (ref_quirks != ts->quirks) {
|
|
|
|
u32 deleted = (ref_quirks ^ ts->quirks) & ref_quirks,
|
|
added = (ref_quirks ^ ts->quirks) & ts->quirks;
|
|
|
|
/* If there is a difference in quirks, but it amounts to 'df' or 'id+'
|
|
disappearing, or 'id-' or 'ecn' appearing, allow a fuzzy match. */
|
|
|
|
if (fmatch || (deleted & ~(QUIRK_DF | QUIRK_NZ_ID)) ||
|
|
(added & ~(QUIRK_ZERO_ID | QUIRK_ECN))) continue;
|
|
|
|
fuzzy = 1;
|
|
|
|
}
|
|
|
|
/* Fixed parameters. */
|
|
|
|
if (refs->opt_eol_pad != ts->opt_eol_pad ||
|
|
refs->ip_opt_len != ts->ip_opt_len) continue;
|
|
|
|
/* TTL matching, with a provision to allow fuzzy match. */
|
|
|
|
if (ref->bad_ttl) {
|
|
|
|
if (refs->ttl < ts->ttl) continue;
|
|
|
|
} else {
|
|
|
|
if (refs->ttl < ts->ttl || refs->ttl - ts->ttl > MAX_DIST) fuzzy = 1;
|
|
|
|
}
|
|
|
|
/* Simple wildcards. */
|
|
|
|
if (refs->mss != -1 && refs->mss != ts->mss) continue;
|
|
if (refs->wscale != -1 && refs->wscale != ts->wscale) continue;
|
|
if (refs->pay_class != -1 && refs->pay_class != ts->pay_class) continue;
|
|
|
|
/* Window size. */
|
|
|
|
if (ts->win_type != WIN_TYPE_NORMAL) {
|
|
|
|
/* Comparing two p0f.fp signatures. */
|
|
|
|
if (refs->win_type != ts->win_type || refs->win != ts->win) continue;
|
|
|
|
} else {
|
|
|
|
/* Comparing real-world stuff. */
|
|
|
|
switch (refs->win_type) {
|
|
|
|
case WIN_TYPE_NORMAL:
|
|
|
|
if (refs->win != ts->win) continue;
|
|
break;
|
|
|
|
case WIN_TYPE_MOD:
|
|
|
|
if (ts->win % refs->win) continue;
|
|
break;
|
|
|
|
case WIN_TYPE_MSS:
|
|
|
|
if (use_mtu || refs->win != win_multi) continue;
|
|
break;
|
|
|
|
case WIN_TYPE_MTU:
|
|
|
|
if (!use_mtu || refs->win != win_multi) continue;
|
|
break;
|
|
|
|
/* WIN_TYPE_ANY */
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* Got a match? If not fuzzy, return. If fuzzy, keep looking. */
|
|
|
|
if (!fuzzy) {
|
|
|
|
if (!ref->generic) {
|
|
|
|
ts->matched = ref;
|
|
ts->fuzzy = 0;
|
|
ts->dist = refs->ttl - ts->ttl;
|
|
return;
|
|
|
|
} else if (!gmatch) gmatch = ref;
|
|
|
|
} else if (!fmatch) fmatch = ref;
|
|
|
|
}
|
|
|
|
/* OK, no definitive match so far... */
|
|
|
|
if (dupe_det) return;
|
|
|
|
/* If we found a generic signature, and nothing better, let's just use
|
|
that. */
|
|
|
|
if (gmatch) {
|
|
|
|
ts->matched = gmatch;
|
|
ts->fuzzy = 0;
|
|
ts->dist = gmatch->sig->ttl - ts->ttl;
|
|
return;
|
|
|
|
}
|
|
|
|
/* No fuzzy matching for userland tools. */
|
|
|
|
if (fmatch && fmatch->class_id == -1) return;
|
|
|
|
/* Let's try to guess distance if no match; or if match TTL out of
|
|
range. */
|
|
|
|
if (!fmatch || fmatch->sig->ttl < ts->ttl ||
|
|
(!fmatch->bad_ttl && fmatch->sig->ttl - ts->ttl > MAX_DIST))
|
|
ts->dist = guess_dist(ts->ttl);
|
|
else
|
|
ts->dist = fmatch->sig->ttl - ts->ttl;
|
|
|
|
/* Record the outcome. */
|
|
|
|
ts->matched = fmatch;
|
|
|
|
if (fmatch) ts->fuzzy = 1;
|
|
|
|
}
|
|
|
|
|
|
/* Parse TCP-specific bits and register a signature read from p0f.fp. This
|
|
function is too long. */
|
|
|
|
void tcp_register_sig(u8 to_srv, u8 generic, s32 sig_class, u32 sig_name,
|
|
u8* sig_flavor, u32 label_id, u32* sys, u32 sys_cnt,
|
|
u8* val, u32 line_no) {
|
|
|
|
s8 ver, win_type, pay_class;
|
|
u8 opt_layout[MAX_TCP_OPT];
|
|
u8 opt_cnt = 0, bad_ttl = 0;
|
|
|
|
s32 ittl, olen, mss, win, scale, opt_eol_pad = 0;
|
|
u32 quirks = 0, bucket, opt_hash;
|
|
|
|
u8* nxt;
|
|
|
|
struct tcp_sig* tsig;
|
|
struct tcp_sig_record* trec;
|
|
|
|
/* IP version */
|
|
|
|
switch (*val) {
|
|
case '4': ver = IP_VER4; break;
|
|
case '6': ver = IP_VER6; break;
|
|
case '*': ver = -1; break;
|
|
default: FATAL("Unrecognized IP version in line %u.", line_no);
|
|
}
|
|
|
|
if (val[1] != ':') FATAL("Malformed signature in line %u.", line_no);
|
|
|
|
val += 2;
|
|
|
|
/* Initial TTL (possibly ttl+dist or ttl-) */
|
|
|
|
nxt = val;
|
|
while (isdigit(*nxt)) nxt++;
|
|
|
|
if (*nxt != ':' && *nxt != '+' && *nxt != '-')
|
|
FATAL("Malformed signature in line %u.", line_no);
|
|
|
|
ittl = atol((char*)val);
|
|
if (ittl < 1 || ittl > 255) FATAL("Bogus initial TTL in line %u.", line_no);
|
|
val = nxt + 1;
|
|
|
|
if (*nxt == '-' && nxt[1] == ':') {
|
|
|
|
bad_ttl = 1;
|
|
val += 2;
|
|
|
|
} else if (*nxt == '+') {
|
|
|
|
s32 ittl_add;
|
|
|
|
nxt++;
|
|
while (isdigit(*nxt)) nxt++;
|
|
if (*nxt != ':') FATAL("Malformed signature in line %u.", line_no);
|
|
|
|
ittl_add = atol((char*)val);
|
|
|
|
if (ittl_add < 0 || ittl + ittl_add > 255)
|
|
FATAL("Bogus initial TTL in line %u.", line_no);
|
|
|
|
ittl += ittl_add;
|
|
val = nxt + 1;
|
|
|
|
}
|
|
|
|
/* Length of IP options */
|
|
|
|
nxt = val;
|
|
while (isdigit(*nxt)) nxt++;
|
|
if (*nxt != ':') FATAL("Malformed signature in line %u.", line_no);
|
|
|
|
olen = atol((char*)val);
|
|
if (olen < 0 || olen > 255)
|
|
FATAL("Bogus IP option length in line %u.", line_no);
|
|
|
|
val = nxt + 1;
|
|
|
|
/* MSS */
|
|
|
|
if (*val == '*' && val[1] == ':') {
|
|
|
|
mss = -1;
|
|
val += 2;
|
|
|
|
} else {
|
|
|
|
nxt = val;
|
|
while (isdigit(*nxt)) nxt++;
|
|
if (*nxt != ':') FATAL("Malformed signature in line %u.", line_no);
|
|
|
|
mss = atol((char*)val);
|
|
if (mss < 0 || mss > 65535) FATAL("Bogus MSS in line %u.", line_no);
|
|
val = nxt + 1;
|
|
|
|
}
|
|
|
|
/* window size, followed by comma */
|
|
|
|
if (*val == '*' && val[1] == ',') {
|
|
|
|
win_type = WIN_TYPE_ANY;
|
|
win = 0;
|
|
val += 2;
|
|
|
|
} else if (*val == '%') {
|
|
|
|
win_type = WIN_TYPE_MOD;
|
|
|
|
val++;
|
|
|
|
nxt = val;
|
|
while (isdigit(*nxt)) nxt++;
|
|
if (*nxt != ',') FATAL("Malformed signature in line %u.", line_no);
|
|
|
|
win = atol((char*)val);
|
|
if (win < 2 || win > 65535) FATAL("Bogus '%%' value in line %u.", line_no);
|
|
val = nxt + 1;
|
|
|
|
} else if (!strncmp((char*)val, "mss*", 4) ||
|
|
!strncmp((char*)val, "mtu*", 4)) {
|
|
|
|
win_type = (val[1] == 's') ? WIN_TYPE_MSS : WIN_TYPE_MTU;
|
|
|
|
val += 4;
|
|
|
|
nxt = val;
|
|
while (isdigit(*nxt)) nxt++;
|
|
if (*nxt != ',') FATAL("Malformed signature in line %u.", line_no);
|
|
|
|
win = atol((char*)val);
|
|
if (win < 1 || win > 1000)
|
|
FATAL("Bogus MSS/MTU multiplier in line %u.", line_no);
|
|
|
|
val = nxt + 1;
|
|
|
|
} else {
|
|
|
|
win_type = WIN_TYPE_NORMAL;
|
|
|
|
nxt = val;
|
|
while (isdigit(*nxt)) nxt++;
|
|
if (*nxt != ',') FATAL("Malformed signature in line %u.", line_no);
|
|
|
|
win = atol((char*)val);
|
|
if (win < 0 || win > 65535) FATAL("Bogus window size in line %u.", line_no);
|
|
val = nxt + 1;
|
|
|
|
}
|
|
|
|
/* Window scale */
|
|
|
|
if (*val == '*' && val[1] == ':') {
|
|
|
|
scale = -1;
|
|
val += 2;
|
|
|
|
} else {
|
|
|
|
nxt = val;
|
|
while (isdigit(*nxt)) nxt++;
|
|
if (*nxt != ':') FATAL("Malformed signature in line %u.", line_no);
|
|
|
|
scale = atol((char*)val);
|
|
if (scale < 0 || scale > 255)
|
|
FATAL("Bogus window scale in line %u.", line_no);
|
|
|
|
val = nxt + 1;
|
|
|
|
}
|
|
|
|
/* Option layout */
|
|
|
|
memset(opt_layout, 0, sizeof(opt_layout));
|
|
|
|
while (*val != ':') {
|
|
|
|
if (opt_cnt >= MAX_TCP_OPT)
|
|
FATAL("Too many TCP options in line %u.", line_no);
|
|
|
|
if (!strncmp((char*)val, "eol", 3)) {
|
|
|
|
opt_layout[opt_cnt++] = TCPOPT_EOL;
|
|
val += 3;
|
|
|
|
if (*val != '+')
|
|
FATAL("Malformed EOL option in line %u.", line_no);
|
|
|
|
val++;
|
|
nxt = val;
|
|
while (isdigit(*nxt)) nxt++;
|
|
|
|
if (!*nxt) FATAL("Truncated options in line %u.", line_no);
|
|
|
|
if (*nxt != ':')
|
|
FATAL("EOL must be the last option in line %u.", line_no);
|
|
|
|
opt_eol_pad = atol((char*)val);
|
|
|
|
if (opt_eol_pad < 0 || opt_eol_pad > 255)
|
|
FATAL("Bogus EOL padding in line %u.", line_no);
|
|
|
|
val = nxt;
|
|
|
|
} else if (!strncmp((char*)val, "nop", 3)) {
|
|
|
|
opt_layout[opt_cnt++] = TCPOPT_NOP;
|
|
val += 3;
|
|
|
|
} else if (!strncmp((char*)val, "mss", 3)) {
|
|
|
|
opt_layout[opt_cnt++] = TCPOPT_MAXSEG;
|
|
val += 3;
|
|
|
|
} else if (!strncmp((char*)val, "ws", 2)) {
|
|
|
|
opt_layout[opt_cnt++] = TCPOPT_WSCALE;
|
|
val += 2;
|
|
|
|
} else if (!strncmp((char*)val, "sok", 3)) {
|
|
|
|
opt_layout[opt_cnt++] = TCPOPT_SACKOK;
|
|
val += 3;
|
|
|
|
} else if (!strncmp((char*)val, "sack", 4)) {
|
|
|
|
opt_layout[opt_cnt++] = TCPOPT_SACK;
|
|
val += 4;
|
|
|
|
} else if (!strncmp((char*)val, "ts", 2)) {
|
|
|
|
opt_layout[opt_cnt++] = TCPOPT_TSTAMP;
|
|
val += 2;
|
|
|
|
} else if (*val == '?') {
|
|
|
|
s32 optno;
|
|
|
|
val++;
|
|
nxt = val;
|
|
while (isdigit(*nxt)) nxt++;
|
|
|
|
if (*nxt != ':' && *nxt != ',')
|
|
FATAL("Malformed '?' option in line %u.", line_no);
|
|
|
|
optno = atol((char*)val);
|
|
|
|
if (optno < 0 || optno > 255)
|
|
FATAL("Bogus '?' option in line %u.", line_no);
|
|
|
|
opt_layout[opt_cnt++] = optno;
|
|
|
|
val = nxt;
|
|
|
|
} else {
|
|
|
|
FATAL("Unrecognized TCP option in line %u.", line_no);
|
|
|
|
}
|
|
|
|
if (*val == ':') break;
|
|
|
|
if (*val != ',')
|
|
FATAL("Malformed TCP options in line %u.", line_no);
|
|
|
|
val++;
|
|
|
|
}
|
|
|
|
val++;
|
|
|
|
opt_hash = hash32(opt_layout, opt_cnt, hash_seed);
|
|
|
|
/* Quirks */
|
|
|
|
while (*val != ':') {
|
|
|
|
if (!strncmp((char*)val, "df", 2)) {
|
|
|
|
if (ver == IP_VER6)
|
|
FATAL("'df' is not valid for IPv6 in line %d.", line_no);
|
|
|
|
quirks |= QUIRK_DF;
|
|
val += 2;
|
|
|
|
} else if (!strncmp((char*)val, "id+", 3)) {
|
|
|
|
if (ver == IP_VER6)
|
|
FATAL("'id+' is not valid for IPv6 in line %d.", line_no);
|
|
|
|
quirks |= QUIRK_NZ_ID;
|
|
val += 3;
|
|
|
|
} else if (!strncmp((char*)val, "id-", 3)) {
|
|
|
|
if (ver == IP_VER6)
|
|
FATAL("'id-' is not valid for IPv6 in line %d.", line_no);
|
|
|
|
quirks |= QUIRK_ZERO_ID;
|
|
val += 3;
|
|
|
|
} else if (!strncmp((char*)val, "ecn", 3)) {
|
|
|
|
quirks |= QUIRK_ECN;
|
|
val += 3;
|
|
|
|
} else if (!strncmp((char*)val, "0+", 2)) {
|
|
|
|
if (ver == IP_VER6)
|
|
FATAL("'0+' is not valid for IPv6 in line %d.", line_no);
|
|
|
|
quirks |= QUIRK_NZ_MBZ;
|
|
val += 2;
|
|
|
|
} else if (!strncmp((char*)val, "flow", 4)) {
|
|
|
|
if (ver == IP_VER4)
|
|
FATAL("'flow' is not valid for IPv4 in line %d.", line_no);
|
|
|
|
quirks |= QUIRK_FLOW;
|
|
val += 4;
|
|
|
|
} else if (!strncmp((char*)val, "seq-", 4)) {
|
|
|
|
quirks |= QUIRK_ZERO_SEQ;
|
|
val += 4;
|
|
|
|
} else if (!strncmp((char*)val, "ack+", 4)) {
|
|
|
|
quirks |= QUIRK_NZ_ACK;
|
|
val += 4;
|
|
|
|
} else if (!strncmp((char*)val, "ack-", 4)) {
|
|
|
|
quirks |= QUIRK_ZERO_ACK;
|
|
val += 4;
|
|
|
|
} else if (!strncmp((char*)val, "uptr+", 5)) {
|
|
|
|
quirks |= QUIRK_NZ_URG;
|
|
val += 5;
|
|
|
|
} else if (!strncmp((char*)val, "urgf+", 5)) {
|
|
|
|
quirks |= QUIRK_URG;
|
|
val += 5;
|
|
|
|
} else if (!strncmp((char*)val, "pushf+", 6)) {
|
|
|
|
quirks |= QUIRK_PUSH;
|
|
val += 6;
|
|
|
|
} else if (!strncmp((char*)val, "ts1-", 4)) {
|
|
|
|
quirks |= QUIRK_OPT_ZERO_TS1;
|
|
val += 4;
|
|
|
|
} else if (!strncmp((char*)val, "ts2+", 4)) {
|
|
|
|
quirks |= QUIRK_OPT_NZ_TS2;
|
|
val += 4;
|
|
|
|
} else if (!strncmp((char*)val, "opt+", 4)) {
|
|
|
|
quirks |= QUIRK_OPT_EOL_NZ;
|
|
val += 4;
|
|
|
|
} else if (!strncmp((char*)val, "exws", 4)) {
|
|
|
|
quirks |= QUIRK_OPT_EXWS;
|
|
val += 4;
|
|
|
|
} else if (!strncmp((char*)val, "bad", 3)) {
|
|
|
|
quirks |= QUIRK_OPT_BAD;
|
|
val += 3;
|
|
|
|
} else {
|
|
|
|
FATAL("Unrecognized quirk in line %u.", line_no);
|
|
|
|
}
|
|
|
|
if (*val == ':') break;
|
|
|
|
if (*val != ',')
|
|
FATAL("Malformed quirks in line %u.", line_no);
|
|
|
|
val++;
|
|
|
|
}
|
|
|
|
val++;
|
|
|
|
/* Payload class */
|
|
|
|
if (!strcmp((char*)val, "*")) pay_class = -1;
|
|
else if (!strcmp((char*)val, "0")) pay_class = 0;
|
|
else if (!strcmp((char*)val, "+")) pay_class = 1;
|
|
else FATAL("Malformed payload class in line %u.", line_no);
|
|
|
|
/* Phew, okay, we're done. Now, create tcp_sig... */
|
|
|
|
tsig = DFL_ck_alloc(sizeof(struct tcp_sig));
|
|
|
|
tsig->opt_hash = opt_hash;
|
|
tsig->opt_eol_pad = opt_eol_pad;
|
|
|
|
tsig->quirks = quirks;
|
|
|
|
tsig->ip_opt_len = olen;
|
|
tsig->ip_ver = ver;
|
|
tsig->ttl = ittl;
|
|
|
|
tsig->mss = mss;
|
|
tsig->win = win;
|
|
tsig->win_type = win_type;
|
|
tsig->wscale = scale;
|
|
tsig->pay_class = pay_class;
|
|
|
|
/* No need to set ts1, recv_ms, match, fuzzy, dist */
|
|
|
|
tcp_find_match(to_srv, tsig, 1, 0);
|
|
|
|
if (tsig->matched)
|
|
FATAL("Signature in line %u is already covered by line %u.",
|
|
line_no, tsig->matched->line_no);
|
|
|
|
/* Everything checks out, so let's register it. */
|
|
|
|
bucket = opt_hash % SIG_BUCKETS;
|
|
|
|
sigs[to_srv][bucket] = DFL_ck_realloc(sigs[to_srv][bucket],
|
|
(sig_cnt[to_srv][bucket] + 1) * sizeof(struct tcp_sig_record));
|
|
|
|
trec = sigs[to_srv][bucket] + sig_cnt[to_srv][bucket];
|
|
|
|
sig_cnt[to_srv][bucket]++;
|
|
|
|
trec->generic = generic;
|
|
trec->class_id = sig_class;
|
|
trec->name_id = sig_name;
|
|
trec->flavor = sig_flavor;
|
|
trec->label_id = label_id;
|
|
trec->sys = sys;
|
|
trec->sys_cnt = sys_cnt;
|
|
trec->line_no = line_no;
|
|
trec->sig = tsig;
|
|
trec->bad_ttl = bad_ttl;
|
|
|
|
/* All done, phew. */
|
|
|
|
}
|
|
|
|
|
|
/* Convert struct packet_data to a simplified struct tcp_sig representation
|
|
suitable for signature matching. Compute hashes. */
|
|
|
|
static void packet_to_sig(struct packet_data* pk, struct tcp_sig* ts) {
|
|
|
|
ts->opt_hash = hash32(pk->opt_layout, pk->opt_cnt, hash_seed);
|
|
|
|
ts->quirks = pk->quirks;
|
|
ts->opt_eol_pad = pk->opt_eol_pad;
|
|
ts->ip_opt_len = pk->ip_opt_len;
|
|
ts->ip_ver = pk->ip_ver;
|
|
ts->ttl = pk->ttl;
|
|
ts->mss = pk->mss;
|
|
ts->win = pk->win;
|
|
ts->win_type = WIN_TYPE_NORMAL; /* Keep as-is. */
|
|
ts->wscale = pk->wscale;
|
|
ts->pay_class = !!pk->pay_len;
|
|
ts->tot_hdr = pk->tot_hdr;
|
|
ts->ts1 = pk->ts1;
|
|
ts->recv_ms = get_unix_time_ms();
|
|
ts->matched = NULL;
|
|
ts->fuzzy = 0;
|
|
ts->dist = 0;
|
|
|
|
};
|
|
|
|
|
|
/* Dump unknown signature. */
|
|
|
|
static u8* dump_sig(struct packet_data* pk, struct tcp_sig* ts, u16 syn_mss) {
|
|
|
|
static u8* ret;
|
|
u32 rlen = 0;
|
|
|
|
u8 win_mtu;
|
|
s16 win_m;
|
|
u32 i;
|
|
u8 dist = guess_dist(pk->ttl);
|
|
|
|
#define RETF(_par...) do { \
|
|
s32 _len = snprintf(NULL, 0, _par); \
|
|
if (_len < 0) FATAL("Whoa, snprintf() fails?!"); \
|
|
ret = DFL_ck_realloc_kb(ret, rlen + _len + 1); \
|
|
snprintf((char*)ret + rlen, _len + 1, _par); \
|
|
rlen += _len; \
|
|
} while (0)
|
|
|
|
if (dist > MAX_DIST) {
|
|
|
|
RETF("%u:%u+?:%u:", pk->ip_ver, pk->ttl, pk->ip_opt_len);
|
|
|
|
} else {
|
|
|
|
RETF("%u:%u+%u:%u:", pk->ip_ver, pk->ttl, dist, pk->ip_opt_len);
|
|
|
|
}
|
|
|
|
/* Detect a system echoing back MSS from p0f-sendsyn queries, suggest using
|
|
a wildcard in such a case. */
|
|
|
|
if (pk->mss == SPECIAL_MSS && pk->tcp_type == (TCP_SYN|TCP_ACK)) RETF("*:");
|
|
else RETF("%u:", pk->mss);
|
|
|
|
win_m = detect_win_multi(ts, &win_mtu, syn_mss);
|
|
|
|
if (win_m > 0) RETF("%s*%u", win_mtu ? "mtu" : "mss", win_m);
|
|
else RETF("%u", pk->win);
|
|
|
|
RETF(",%u:", pk->wscale);
|
|
|
|
for (i = 0; i < pk->opt_cnt; i++) {
|
|
|
|
switch (pk->opt_layout[i]) {
|
|
|
|
case TCPOPT_EOL:
|
|
RETF("%seol+%u", i ? "," : "", pk->opt_eol_pad); break;
|
|
|
|
case TCPOPT_NOP:
|
|
RETF("%snop", i ? "," : ""); break;
|
|
|
|
case TCPOPT_MAXSEG:
|
|
RETF("%smss", i ? "," : ""); break;
|
|
|
|
case TCPOPT_WSCALE:
|
|
RETF("%sws", i ? "," : ""); break;
|
|
|
|
case TCPOPT_SACKOK:
|
|
RETF("%ssok", i ? "," : ""); break;
|
|
|
|
case TCPOPT_SACK:
|
|
RETF("%ssack", i ? "," : ""); break;
|
|
|
|
case TCPOPT_TSTAMP:
|
|
RETF("%sts", i ? "," : ""); break;
|
|
|
|
default:
|
|
RETF("%s?%u", i ? "," : "", pk->opt_layout[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
RETF(":");
|
|
|
|
if (pk->quirks) {
|
|
|
|
u8 sp = 0;
|
|
|
|
#define MAYBE_CM(_str) do { \
|
|
if (sp) RETF("," _str); else RETF(_str); \
|
|
sp = 1; \
|
|
} while (0)
|
|
|
|
if (pk->quirks & QUIRK_DF) MAYBE_CM("df");
|
|
if (pk->quirks & QUIRK_NZ_ID) MAYBE_CM("id+");
|
|
if (pk->quirks & QUIRK_ZERO_ID) MAYBE_CM("id-");
|
|
if (pk->quirks & QUIRK_ECN) MAYBE_CM("ecn");
|
|
if (pk->quirks & QUIRK_NZ_MBZ) MAYBE_CM("0+");
|
|
if (pk->quirks & QUIRK_FLOW) MAYBE_CM("flow");
|
|
|
|
if (pk->quirks & QUIRK_ZERO_SEQ) MAYBE_CM("seq-");
|
|
if (pk->quirks & QUIRK_NZ_ACK) MAYBE_CM("ack+");
|
|
if (pk->quirks & QUIRK_ZERO_ACK) MAYBE_CM("ack-");
|
|
if (pk->quirks & QUIRK_NZ_URG) MAYBE_CM("uptr+");
|
|
if (pk->quirks & QUIRK_URG) MAYBE_CM("urgf+");
|
|
if (pk->quirks & QUIRK_PUSH) MAYBE_CM("pushf+");
|
|
|
|
if (pk->quirks & QUIRK_OPT_ZERO_TS1) MAYBE_CM("ts1-");
|
|
if (pk->quirks & QUIRK_OPT_NZ_TS2) MAYBE_CM("ts2+");
|
|
if (pk->quirks & QUIRK_OPT_EOL_NZ) MAYBE_CM("opt+");
|
|
if (pk->quirks & QUIRK_OPT_EXWS) MAYBE_CM("exws");
|
|
if (pk->quirks & QUIRK_OPT_BAD) MAYBE_CM("bad");
|
|
|
|
#undef MAYBE_CM
|
|
|
|
}
|
|
|
|
if (pk->pay_len) RETF(":+"); else RETF(":0");
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
/* Dump signature-related flags. */
|
|
|
|
static u8* dump_flags(struct packet_data* pk, struct tcp_sig* ts) {
|
|
|
|
static u8* ret;
|
|
u32 rlen = 0;
|
|
|
|
RETF("");
|
|
|
|
if (ts->matched) {
|
|
|
|
if (ts->matched->generic) RETF(" generic");
|
|
if (ts->fuzzy) RETF(" fuzzy");
|
|
if (ts->matched->bad_ttl) RETF(" random_ttl");
|
|
|
|
}
|
|
|
|
if (ts->dist > MAX_DIST) RETF(" excess_dist");
|
|
if (pk->tos) RETF(" tos:0x%02x", pk->tos);
|
|
|
|
if (*ret) return ret + 1; else return (u8*)"none";
|
|
|
|
#undef RETF
|
|
|
|
}
|
|
|
|
|
|
/* Compare current signature with historical data, draw conclusions. This
|
|
is called only for OS sigs. */
|
|
|
|
static void score_nat(u8 to_srv, struct tcp_sig* sig, struct packet_flow* f) {
|
|
|
|
struct host_data* hd;
|
|
struct tcp_sig* ref;
|
|
u8 score = 0, diff_already = 0;
|
|
u16 reason = 0;
|
|
s32 ttl_diff;
|
|
|
|
if (to_srv) {
|
|
|
|
hd = f->client;
|
|
ref = hd->last_syn;
|
|
|
|
} else {
|
|
|
|
hd = f->server;
|
|
ref = hd->last_synack;
|
|
|
|
}
|
|
|
|
|
|
if (!ref) {
|
|
|
|
/* No previous signature of matching type at all. We can perhaps still check
|
|
if class / name is the same as on file, as that data might have been
|
|
obtained from other types of sigs. */
|
|
|
|
if (sig->matched && hd->last_class_id != -1) {
|
|
|
|
if (hd->last_name_id != sig->matched->name_id) {
|
|
|
|
DEBUG("[#] New TCP signature different OS type than host data.\n");
|
|
|
|
reason |= NAT_OS_SIG;
|
|
score += 8;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
goto log_and_update;
|
|
|
|
}
|
|
|
|
/* We have some previous data. */
|
|
|
|
if (!sig->matched || !ref->matched) {
|
|
|
|
/* One or both of the signatures are unknown. Let's see if they differ.
|
|
The scoring here isn't too strong, because we don't know if the
|
|
unrecognized signature isn't originating from userland tools. */
|
|
|
|
if ((sig->quirks ^ ref->quirks) & ~(QUIRK_ECN|QUIRK_DF|QUIRK_NZ_ID|
|
|
QUIRK_ZERO_ID)) {
|
|
|
|
DEBUG("[#] Non-fuzzy quirks changed compared to previous sig.\n");
|
|
|
|
reason |= NAT_UNK_DIFF;
|
|
score += 2;
|
|
|
|
} else if (to_srv && sig->opt_hash != ref->opt_hash) {
|
|
|
|
/* We only match option layout for SYNs; it may change on SYN+ACK,
|
|
and the user may have gaps in SYN+ACK sigs if he ignored our
|
|
advice on using p0f-sendsyn. */
|
|
|
|
DEBUG("[#] SYN option layout changed compared to previous sig.\n");
|
|
|
|
reason |= NAT_UNK_DIFF;
|
|
score += 1;
|
|
|
|
}
|
|
|
|
/* Progression from known to unknown is also of interest for SYNs. */
|
|
|
|
if (to_srv && sig->matched != ref->matched) {
|
|
|
|
DEBUG("[#] SYN signature changed from %s.\n",
|
|
sig->matched ? "unknown to known" : "known to unknown");
|
|
|
|
score += 1;
|
|
reason |= NAT_TO_UNK;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
/* Both signatures known! */
|
|
|
|
if (ref->matched->name_id != sig->matched->name_id) {
|
|
|
|
DEBUG("[#] TCP signature different OS type on previous sig.\n");
|
|
score += 8;
|
|
reason |= NAT_OS_SIG;
|
|
|
|
diff_already = 1;
|
|
|
|
} else if (to_srv) {
|
|
|
|
/* SYN signatures match superficially, but... */
|
|
|
|
if (ref->matched->label_id != sig->matched->label_id) {
|
|
|
|
/* SYN label changes are a weak but useful signal. SYN+ACK signatures
|
|
may need less intuitive groupings, so we don't check that. */
|
|
|
|
DEBUG("[#] SYN signature label different on previous sig.\n");
|
|
score += 2;
|
|
reason |= NAT_OS_SIG;
|
|
|
|
} else if (ref->matched->line_no != sig->matched->line_no) {
|
|
|
|
/* Change in line number is an extremely weak but still noteworthy
|
|
signal. */
|
|
|
|
DEBUG("[#] SYN signature changes within the same label.\n");
|
|
score += 1;
|
|
reason |= NAT_OS_SIG;
|
|
|
|
} else if (sig->fuzzy != ref->fuzzy) {
|
|
|
|
/* Fuzziness change on a perfectly matched signature? */
|
|
|
|
DEBUG("[#] SYN signature fuzziness changes.\n");
|
|
score += 1;
|
|
reason |= NAT_FUZZY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* Unless the signatures are already known to differ radically, mismatch
|
|
between host data and current sig is of additional note. */
|
|
|
|
if (!diff_already && sig->matched && hd->last_class_id != -1 &&
|
|
hd->last_name_id != sig->matched->name_id) {
|
|
|
|
DEBUG("[#] New OS signature different OS type than host data.\n");
|
|
score += 8;
|
|
reason |= NAT_OS_SIG;
|
|
diff_already = 1;
|
|
|
|
}
|
|
|
|
/* TTL differences in absence of major signature mismatches is also
|
|
interesting, unless the signatures are tagged as "bad TTL", or unless
|
|
the difference is barely 1 and the host is distant. */
|
|
|
|
#define ABS(_x) ((_x) < 0 ? -(_x) : (_x))
|
|
|
|
ttl_diff = ((s16)sig->ttl) - ref->ttl;
|
|
|
|
if (!diff_already && ttl_diff && (!sig->matched || !sig->matched->bad_ttl) &&
|
|
(!ref->matched || !ref->matched->bad_ttl) && (sig->dist <= NEAR_TTL_LIMIT ||
|
|
ttl_diff > 1)) {
|
|
|
|
DEBUG("[#] Signature TTL differs by %d (dist = %u).\n", ttl_diff, sig->dist);
|
|
|
|
if (sig->dist > LOCAL_TTL_LIMIT && ABS(ttl_diff) <= SMALL_TTL_CHG)
|
|
score += 1; else score += 4;
|
|
|
|
reason |= NAT_TTL;
|
|
|
|
}
|
|
|
|
/* Source port going back frequently is of some note, although it will happen
|
|
spontaneously every now and then. Require the drop to be by at least
|
|
few dozen, to account for simple case of several simultaneously opened
|
|
connections arriving in odd order. */
|
|
|
|
if (to_srv && hd->last_port && f->cli_port < hd->last_port &&
|
|
hd->last_port - f->cli_port >= MIN_PORT_DROP) {
|
|
|
|
DEBUG("[#] Source port drops from %u to %u.\n", hd->last_port, f->cli_port);
|
|
|
|
score += 1;
|
|
reason |= NAT_PORT;
|
|
|
|
}
|
|
|
|
/* Change of MTU is always sketchy. */
|
|
|
|
if (sig->mss != ref->mss) {
|
|
|
|
DEBUG("[#] MSS for signature changed from %u to %u.\n", ref->mss, sig->mss);
|
|
|
|
score += 1;
|
|
reason |= NAT_MSS;
|
|
|
|
}
|
|
|
|
/* Check timestamp progression to possibly adjust current score. Don't rate
|
|
on TS alone, because some systems may be just randomizing that. */
|
|
|
|
if (score && sig->ts1 && ref->ts1) {
|
|
|
|
u64 ms_diff = sig->recv_ms - ref->recv_ms;
|
|
|
|
/* Require a timestamp within the last day; if the apparent TS progression
|
|
is much higher than 1 kHz, complain. */
|
|
|
|
if (ms_diff < MAX_NAT_TS) {
|
|
|
|
u64 use_ms = (ms_diff < TSTAMP_GRACE) ? TSTAMP_GRACE : ms_diff;
|
|
u64 max_ts = use_ms * MAX_TSCALE / 1000;
|
|
|
|
u32 ts_diff = sig->ts1 - ref->ts1;
|
|
|
|
if (ts_diff > max_ts && (ms_diff >= TSTAMP_GRACE || ~ts_diff > max_ts)) {
|
|
|
|
DEBUG("[#] Dodgy timestamp progression across signatures (%d "
|
|
"in %llu ms).\n", ts_diff, ms_diff);
|
|
score += 4;
|
|
reason |= NAT_TS;
|
|
|
|
} else {
|
|
|
|
DEBUG("[#] Timestamp consistent across signatures (%d in %llu ms), "
|
|
"reducing score.\n", ts_diff, ms_diff);
|
|
score /= 2;
|
|
|
|
}
|
|
|
|
} else DEBUG("[#] Timestamps available, but with bad interval (%llu ms).\n",
|
|
ms_diff);
|
|
|
|
}
|
|
|
|
log_and_update:
|
|
|
|
add_nat_score(to_srv, f, reason, score);
|
|
|
|
/* Update some of the essential records. */
|
|
|
|
if (sig->matched) {
|
|
|
|
hd->last_class_id = sig->matched->class_id;
|
|
hd->last_name_id = sig->matched->name_id;
|
|
hd->last_flavor = sig->matched->flavor;
|
|
|
|
hd->last_quality = (sig->fuzzy * P0F_MATCH_FUZZY) |
|
|
(sig->matched->generic * P0F_MATCH_GENERIC);
|
|
|
|
}
|
|
|
|
hd->last_port = f->cli_port;
|
|
|
|
}
|
|
|
|
|
|
/* Fingerprint SYN or SYN+ACK. */
|
|
|
|
struct tcp_sig* fingerprint_tcp(u8 to_srv, struct packet_data* pk,
|
|
struct packet_flow* f) {
|
|
|
|
struct tcp_sig* sig;
|
|
struct tcp_sig_record* m;
|
|
|
|
sig = ck_alloc(sizeof(struct tcp_sig));
|
|
packet_to_sig(pk, sig);
|
|
|
|
/* Detect packets generated by p0f-sendsyn; they require special
|
|
handling to provide the user with response fingerprints, but not
|
|
interfere with NAT scores and such. */
|
|
|
|
if (pk->tcp_type == TCP_SYN && pk->win == SPECIAL_WIN &&
|
|
pk->mss == SPECIAL_MSS) f->sendsyn = 1;
|
|
|
|
if (to_srv)
|
|
start_observation(f->sendsyn ? "sendsyn probe" : "syn", 4, 1, f);
|
|
else
|
|
start_observation(f->sendsyn ? "sendsyn response" : "syn+ack", 4, 0, f);
|
|
|
|
tcp_find_match(to_srv, sig, 0, f->syn_mss);
|
|
|
|
if ((m = sig->matched)) {
|
|
|
|
OBSERVF((m->class_id == -1 || f->sendsyn) ? "app" : "os", "%s%s%s",
|
|
fp_os_names[m->name_id], m->flavor ? " " : "",
|
|
m->flavor ? m->flavor : (u8*)"");
|
|
|
|
} else {
|
|
|
|
add_observation_field("os", NULL);
|
|
|
|
}
|
|
|
|
if (m && m->bad_ttl) {
|
|
|
|
OBSERVF("dist", "<= %u", sig->dist);
|
|
|
|
} else {
|
|
|
|
if (to_srv) f->client->distance = sig->dist;
|
|
else f->server->distance = sig->dist;
|
|
|
|
OBSERVF("dist", "%u", sig->dist);
|
|
|
|
}
|
|
|
|
add_observation_field("params", dump_flags(pk, sig));
|
|
|
|
add_observation_field("raw_sig", dump_sig(pk, sig, f->syn_mss));
|
|
|
|
if (pk->tcp_type == TCP_SYN) f->syn_mss = pk->mss;
|
|
|
|
/* That's about as far as we go with non-OS signatures. */
|
|
|
|
if (m && m->class_id == -1) {
|
|
verify_tool_class(to_srv, f, m->sys, m->sys_cnt);
|
|
ck_free(sig);
|
|
return NULL;
|
|
}
|
|
|
|
if (f->sendsyn) {
|
|
ck_free(sig);
|
|
return NULL;
|
|
}
|
|
|
|
score_nat(to_srv, sig, f);
|
|
|
|
return sig;
|
|
|
|
}
|
|
|
|
|
|
/* Perform uptime detection. This is the only FP function that gets called not
|
|
only on SYN or SYN+ACK, but also on ACK traffic. */
|
|
|
|
void check_ts_tcp(u8 to_srv, struct packet_data* pk, struct packet_flow* f) {
|
|
|
|
u32 ts_diff;
|
|
u64 ms_diff;
|
|
|
|
u32 freq;
|
|
u32 up_min, up_mod_days;
|
|
|
|
double ffreq;
|
|
|
|
if (!pk->ts1 || f->sendsyn) return;
|
|
|
|
/* If we're getting SYNs very rapidly, last_syn may be changing too quickly
|
|
to be of any use. Perhaps lock into an older value? */
|
|
|
|
if (to_srv) {
|
|
|
|
if (f->cli_tps || !f->client->last_syn || !f->client->last_syn->ts1)
|
|
return;
|
|
|
|
ms_diff = get_unix_time_ms() - f->client->last_syn->recv_ms;
|
|
ts_diff = pk->ts1 - f->client->last_syn->ts1;
|
|
|
|
} else {
|
|
|
|
if (f->srv_tps || !f->server->last_synack || !f->server->last_synack->ts1)
|
|
return;
|
|
|
|
ms_diff = get_unix_time_ms() - f->server->last_synack->recv_ms;
|
|
ts_diff = pk->ts1 - f->server->last_synack->ts1;
|
|
|
|
}
|
|
|
|
/* Wait at least 25 ms, and not more than 10 minutes, for at least 5
|
|
timestamp ticks. Allow the timestamp to go back slightly within
|
|
a short window, too - we may be receiving packets a bit out of
|
|
order. */
|
|
|
|
if (ms_diff < MIN_TWAIT || ms_diff > MAX_TWAIT) return;
|
|
|
|
if (ts_diff < 5 || (ms_diff < TSTAMP_GRACE && (~ts_diff) / 1000 <
|
|
MAX_TSCALE / TSTAMP_GRACE)) return;
|
|
|
|
if (ts_diff > ~ts_diff) ffreq = ~ts_diff * -1000.0 / ms_diff;
|
|
else ffreq = ts_diff * 1000.0 / ms_diff;
|
|
|
|
if (ffreq < MIN_TSCALE || ffreq > MAX_TSCALE) {
|
|
|
|
/* Allow bad reading on SYN, as this may be just an artifact of IP
|
|
sharing or OS change. */
|
|
|
|
if (pk->tcp_type != TCP_SYN) {
|
|
|
|
if (to_srv) f->cli_tps = -1; else f->srv_tps = -1;
|
|
|
|
}
|
|
|
|
DEBUG("[#] Bad %s TS frequency: %.02f Hz (%d ticks in %llu ms).\n",
|
|
to_srv ? "client" : "server", ffreq, ts_diff, ms_diff);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
freq = ffreq;
|
|
|
|
/* Round the frequency neatly. */
|
|
|
|
switch (freq) {
|
|
|
|
case 0: freq = 1; break;
|
|
case 1 ... 10: break;
|
|
case 11 ... 50: freq = (freq + 3) / 5 * 5; break;
|
|
case 51 ... 100: freq = (freq + 7) / 10 * 10; break;
|
|
case 101 ... 500: freq = (freq + 33) / 50 * 50; break;
|
|
default: freq = (freq + 67) / 100 * 100; break;
|
|
|
|
}
|
|
|
|
if (to_srv) f->cli_tps = freq; else f->srv_tps = freq;
|
|
|
|
up_min = pk->ts1 / freq / 60;
|
|
up_mod_days = 0xFFFFFFFF / (freq * 60 * 60 * 24);
|
|
|
|
start_observation("uptime", 2, to_srv, f);
|
|
|
|
if (to_srv) {
|
|
|
|
f->client->last_up_min = up_min;
|
|
f->client->up_mod_days = up_mod_days;
|
|
|
|
} else {
|
|
|
|
f->server->last_up_min = up_min;
|
|
f->server->up_mod_days = up_mod_days;
|
|
|
|
}
|
|
|
|
OBSERVF("uptime", "%u days %u hrs %u min (modulo %u days)",
|
|
(up_min / 60 / 24), (up_min / 60) % 24, up_min % 60,
|
|
up_mod_days);
|
|
|
|
OBSERVF("raw_freq", "%.02f Hz", ffreq);
|
|
|
|
}
|