Changeset 11127 in ntrip


Ignore:
Timestamp:
Oct 24, 2026, 11:15:22 PM (5 hours ago)
Author:
stuerze
Message:

test suite extended regardiing security aspects

Location:
trunk/BNC/tests
Files:
2 added
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/BNC/tests/README.md

    r11124 r11127  
    2626| `cases/<name>/expected/*.gz` | normalised reference outputs |
    2727| `data/*.raw` | short raw captures (BNC `rawOutFile` format) replayed via `--file` |
    28 | `tools/fakecaster.py` | local Ntrip caster recording an upload as a BNC raw file (case key `fakecaster`) |
    29 | `tools/bncraw.py` | `info` (streams, time span, RTCM3 message types) and `slice` for raw captures |
     28| `tools/fakecaster.py` | local Ntrip caster recording an upload as a BNC raw file (case key `fakecaster`); optionally TLS and serving a stream |
     29| `tools/bncraw.py` | `info` (streams, time span, RTCM3 message types), `slice` and `crs` (decode 1300/1301/1302) for raw captures |
    3030| `tools/mkcrafted.py` | writes `data/crafted_limits.raw`: crafted frames at the upper field limits |
    3131| `tools/ssr_harness.sh` | builds `tools/ssr_harness.cpp` with the SSR decoders under ASan/UBSan and runs it |
     32| `tools/ssl_check.py` | TLS checks of the Ntrip 2s client and upload (not part of `run_tests.py`) |
    3233| `work/` | scratch output, not versioned |
    3334
    … …  
    7576  (`tools/fakecaster.py`, Ntrip 1/2, chunked), and a `post` step decodes the
    7677  received stream again: `upload_rtnet` (IGS-SSR encoding, SP3/CLK/BIA files,
    77   decoded `.ssr`), `upload_eph` (RTCM3 ephemerides to RINEX nav) and
     78  decoded `.ssr`, CRS messages 1300/1302 decoded by `bncraw.py crs` in a
     79  `tool` post step), `upload_eph` (RTCM3 ephemerides to RINEX nav) and
    7880  `upload_raw` (raw forwarding to RINEX nav). `upload_rtnet_rtcm` encodes the same input to RTCM-SSR
    7981  (message numbers 1057-1068, 1240-1270) and decodes it again.
    … …  
    114116`crafted_limits.raw`: 7 entries with additional parameters of 40 and 0 bits,
    115117and 31 entries, which must be rejected), which `RTCM3Decoder.cpp` does not
    116 dispatch at present. Exit code 0 only without sanitizer errors.
     118dispatch at present. Every decoded VTEC and metadata message is encoded
     119again with the `Make*` function of the same class and decoded once more; the
     120result must be identical. Exit code 0 only without sanitizer errors and
     121differences.
     122
     123## TLS check
     124
     125```
     126tests/tools/ssl_check.py [--bnc PATH]
     127```
     128
     129Runs BNC against `tools/fakecaster.py` with TLS on port 443 (BNC uploads with
     130Ntrip 2s always to 443) inside an unprivileged user and network namespace
     131(`unshare -rn`), with a test CA, server and client certificates created by
     132openssl in `work/ssl_check/`. Checks for upload and download: certificate of
     133an unknown CA rejected, accepted with the CA in `sslCaCertPath`, accepted with
     134`sslIgnoreErrors` and the ignored errors logged; for upload also a
     135certificate for another host name (rejected) and a client certificate
     136required by the caster, with and without `sslIgnoreErrors`. Takes about 3 min.
    117137
    118138## Adding a replay case
  • trunk/BNC/tests/cases/upload_rtnet/case.json

    r11074 r11127  
    11{
    2   "description": "16_Upload replay: RTNET orbits/clocks/biases + BCEP00BKG0 encoded to IGS-SSR and uploaded (Ntrip 2, chunked) to a local fake caster; SP3/CLK/BIA written; the uploaded stream is decoded again (03_BrdcCorr), 5 min",
     2  "description": "16_Upload replay: RTNET orbits/clocks/biases + BCEP00BKG0 encoded to IGS-SSR and uploaded (Ntrip 2, chunked) to a local fake caster; SP3/CLK/BIA written; the uploaded stream is decoded again (03_BrdcCorr), CRS messages 1300/1302 decoded by bncraw.py crs, 5 min",
    33  "config": "Example_Configs/16_Upload.bnc",
    44  "raw": "data/upload_rtnet_5min.raw",
    … …  
    1010                 "time": "2026-10-01T09:33:00", "timeout": 10},
    1111  "post": [{"dir": "dec", "config": "Example_Configs/03_BrdcCorr.bnc",
    12             "raw": "Output/upload.raw", "exit_code": 5, "timeout": 300}],
     12            "raw": "Output/upload.raw", "exit_code": 5, "timeout": 300},
     13           {"dir": "crs", "tool": ["bncraw.py", "crs", "Output/upload.raw"]}],
    1314  "outputs": ["Output/*.SP3", "Output/*.CLK", "Output/*.BIA",
    1415              {"glob": "Output/Upload.log_261001", "name": "Upload.log"},
    1516              "dec/Output/*.ssr",
     17              {"glob": "crs/stdout.txt", "name": "upload_crs.txt"},
    1618              {"glob": "dec/stderr.txt", "name": "decode_trace.txt", "debug_trace": true}]
    1719}
  • trunk/BNC/tests/run_tests.py

    r11069 r11127  
    3030               {"dir" (subdirectory of the work dir), "config", "raw"
    3131                (relative to the work dir), "args", "exit_code", "timeout"};
    32                outputs may refer to their files as "<dir>/Output/..."
     32               outputs may refer to their files as "<dir>/Output/...";
     33               instead of a BNC run, {"dir", "tool": [script in tools/, args]}
     34               runs a Python tool in the work directory, stdout to <dir>/stdout.txt
    3335
    3436BNC runs in tests/work/<name>/, which contains an "Input" symlink to
    … …  
    178180
    179181
     182def run_tool(work, spec):
     183    """Run tools/<script> with args in the work directory, stdout to
     184    <dir>/stdout.txt. Returns the exit code or None on timeout."""
     185    outdir = os.path.join(work, spec["dir"])
     186    os.makedirs(outdir, exist_ok=True)
     187    cmd = [sys.executable, "-I", os.path.join(TESTS, "tools", spec["tool"][0])] + spec["tool"][1:]
     188    with open(os.path.join(outdir, "stdout.txt"), "wb") as out:
     189        try:
     190            return subprocess.run(cmd, cwd=work, stdout=out, stderr=subprocess.STDOUT,
     191                                  timeout=spec.get("timeout", 600)).returncode
     192        except subprocess.TimeoutExpired:
     193            return None
     194
     195
    180196def run_case(name, bnc, update):
    181197    res = Result(name)
    … …  
    217233    # in its own subdirectory of the work directory, raw relative to work/
    218234    for post in case.get("post", []):
    219         pcode = run_bnc(bnc, os.path.join(work, post["dir"]), post, work)
     235        if "tool" in post:
     236            pcode = run_tool(work, post)
     237        else:
     238            pcode = run_bnc(bnc, os.path.join(work, post["dir"]), post, work)
    220239        if pcode is None or pcode != post.get("exit_code", 0):
    221240            res.fail("post step %s: exit code %s" % (post["dir"], pcode))
    … …  
    250269        expfile = os.path.join(expdir, ref + ".gz")
    251270        if update:
     271            if spec.get("debug_trace") and not actual.strip() and os.path.exists(expfile):
     272                # build without BNC_DEBUG_SSR: keep the existing trace reference
     273                written.add(os.path.basename(expfile))
     274                res.skip("%s: no BNC_DEBUG_SSR trace in this build, reference kept" % ref)
     275                continue
    252276            write_expected(expfile, actual)
    253277            written.add(os.path.basename(expfile))
  • trunk/BNC/tests/tools/bncraw.py

    r11069 r11127  
    99    info  <raw> [<raw> ...]          streams, byte counts, time span, RTCM3 message types
    1010    slice <raw> <out> [options]      cut a time window / subset of stations
     11    crs   <raw>                      decode RTCM 1300/1301/1302 (CRS messages)
    1112"""
    1213
    … …  
    110111        return "4076_%03d" % sub
    111112    return str(t)
     113
     114
     115class BitReader:
     116    def __init__(self, data):
     117        self.data = data
     118        self.pos = 0
     119
     120    def u(self, n):
     121        v = 0
     122        for _ in range(n):
     123            if self.pos >= 8 * len(self.data):
     124                raise ValueError("message too short")
     125            v = (v << 1) | ((self.data[self.pos // 8] >> (7 - self.pos % 8)) & 1)
     126            self.pos += 1
     127        return v
     128
     129    def s(self, n):
     130        v = self.u(n)
     131        return v - (1 << n) if v >> (n - 1) else v
     132
     133    def text(self):
     134        n = self.u(5)
     135        return bytes(self.u(8) for _ in range(n)).decode("latin-1")
     136
     137    def left(self):
     138        return 8 * len(self.data) - self.pos
     139
     140
     141def decode_crs(p):
     142    """One line per 1300/1301/1302 message (RTCM 10403.4, DF562-DF587)."""
     143    r = BitReader(p)
     144    t = r.u(12)
     145    if t == 1300:
     146        out = "1300 name %r CE %.2f" % (r.text(), r.u(16) / 100.0)
     147    elif t == 1301:
     148        src, tgt = r.text(), r.text()
     149        out = "1301 source %r target %r sys %d util %d mjd %d par" % (
     150            src, tgt, r.u(8), r.u(10), r.u(16) + 44244)
     151        for n in (23, 23, 23, 32, 32, 32, 25, 17, 17, 17, 17, 17, 17, 14):
     152            out += " %d" % r.s(n)
     153    else:
     154        out = "1302 name %r anchor %d plate %d links" % (r.text(), r.u(1), r.u(5))
     155        out += " %r" % [r.text() for _ in range(r.u(3))]
     156    if r.left() >= 8:  # more than the padding to the byte boundary
     157        out += "  TRAILING %d bits" % r.left()
     158    return out
    112159
    113160
    … …  
    166213
    167214
     215def cmd_crs(args):
     216    """Distinct CRS message contents per stream, with their number."""
     217    stats = collections.OrderedDict()
     218    framers = {}
     219    for t, sta, fmt, data in read_chunks(args.input):
     220        for p in framers.setdefault(sta, Rtcm3Framer()).feed(data):
     221            if msg_type(p) in ("1300", "1301", "1302"):
     222                try:
     223                    line = decode_crs(p)
     224                except ValueError as e:
     225                    line = "%s %s" % (msg_type(p), e)
     226                key = (sta, line)
     227                stats[key] = stats.get(key, 0) + 1
     228    for (sta, line), n in stats.items():
     229        print("%s %4dx %s" % (sta, n, line))
     230
     231
    168232def main():
    169233    ap = argparse.ArgumentParser(description=__doc__,
    … …  
    183247    p.set_defaults(func=cmd_slice)
    184248
     249    p = sub.add_parser("crs", help="decode CRS messages 1300/1301/1302")
     250    p.add_argument("input")
     251    p.set_defaults(func=cmd_crs)
     252
    185253    args = ap.parse_args()
    186254    args.func(args)
  • trunk/BNC/tests/tools/fakecaster.py

    r11074 r11127  
    1111
    1212    fakecaster.py --port 21011 --out upload.raw --sta CLOCK --time 2026-10-01T09:30:00
     13
     14With --tls-cert/--tls-key the caster speaks TLS (Ntrip 2s); --tls-client-ca
     15additionally requires a client certificate signed by that CA. With --serve
     16a GET request (BNC as Ntrip client) is answered with the RTCM bytes of the
     17given raw file. Failed TLS handshakes are reported on stdout.
    1318"""
    1419
    1520import argparse
     21import os
    1622import socket
     23import ssl
    1724import sys
    1825import threading
     26
     27sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
    1928
    2029
    … …  
    4554        self.head = "%s %s %s" % (time, sta, fmt)
    4655        self.total = 0
     56        self.handshakes = 0
     57        self.served = 0
    4758
    4859    def write(self, data):
    … …  
    5667
    5768
    58 def serve(conn, store, timeout):
     69def serve(conn, store, timeout, tls=None, served=None):
    5970    conn.settimeout(timeout)
    6071    buf = b""
    6172    try:
     73        if tls:
     74            try:
     75                conn = tls.wrap_socket(conn, server_side=True)
     76            except (ssl.SSLError, OSError) as e:
     77                print("tls handshake failed: %s" % e, flush=True)
     78                return
     79            store.handshakes += 1
    6280        while b"\r\n\r\n" not in buf:
    6381            d = conn.recv(4096)
    … …  
    6785        header, _, pending = buf.partition(b"\r\n\r\n")
    6886        chunked = b"chunked" in header.lower()
     87        if header.startswith(b"GET") and served is not None:
     88            conn.sendall(b"HTTP/1.1 200 OK\r\nNtrip-Version: Ntrip/2.0\r\n"
     89                         b"Content-Type: gnss/data\r\nConnection: close\r\n\r\n" + served)
     90            store.served += len(served)
     91            return
    6992        if header.startswith(b"SOURCE"):
    7093            conn.sendall(b"ICY 200 OK\r\n\r\n")
    … …  
    100123    ap.add_argument("--timeout", type=float, default=600.0,
    101124                    help="stop after this many seconds without a new connection or data")
     125    ap.add_argument("--tls-cert", help="server certificate (PEM): TLS")
     126    ap.add_argument("--tls-key", help="server private key (PEM)")
     127    ap.add_argument("--tls-client-ca", help="require a client certificate of this CA")
     128    ap.add_argument("--serve", help="raw file whose RTCM bytes are sent on GET")
    102129    args = ap.parse_args()
     130
     131    tls = None
     132    if args.tls_cert:
     133        tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
     134        tls.load_cert_chain(args.tls_cert, args.tls_key)
     135        if args.tls_client_ca:
     136            tls.verify_mode = ssl.CERT_REQUIRED
     137            tls.load_verify_locations(args.tls_client_ca)
     138    served = None
     139    if args.serve:
     140        from bncraw import read_chunks  # noqa: E402
     141        served = b"".join(d for t, sta, fmt, d in read_chunks(args.serve))
    103142
    104143    store = Store(args.out, args.sta, args.format, args.time)
    … …  
    116155        except socket.timeout:
    117156            break
    118         t = threading.Thread(target=serve, args=(conn, store, args.timeout), daemon=True)
     157        t = threading.Thread(target=serve, args=(conn, store, args.timeout, tls, served),
     158                             daemon=True)
    119159        t.start()
    120160        threads.append(t)
    … …  
    122162        t.join(args.timeout)
    123163    print("received %d bytes in %d connection(s)" % (store.total, len(threads)), flush=True)
     164    if tls:
     165        print("%d TLS handshake(s) completed" % store.handshakes, flush=True)
     166    if served is not None:
     167        print("served %d bytes" % store.served, flush=True)
    124168    return 0
    125169
  • trunk/BNC/tests/tools/ssr_harness.cpp

    r11125 r11127  
    66// struct is a separate heap block here, so a write beyond a struct is
    77// reported by AddressSanitizer instead of silently corrupting the next member.
     8//
     9// Every decoded VTEC (all formats) and metadata message (new RTCM-SSR) is
     10// encoded again with the Make* function of the same class and decoded once
     11// more; the result must be identical (exit code 1 otherwise).
    812//
    913// Usage: ssr_harness <file>   (BNC raw file or plain RTCM3 byte stream)
    … …  
    3337}
    3438
     39struct t_target {
     40  t_target()  : co(new SsrCorr::ClockOrbit()), cb(new SsrCorr::CodeBias()),
     41                v(new SsrCorr::VTEC()), pb(new SsrCorr::PhaseBias()),
     42                md(new SsrCorr::MetaData()), a(new SsrCorr::Antenna()) {}
     43  ~t_target() {delete co; delete cb; delete v; delete pb; delete md; delete a;}
     44  int decode(SsrCorr* ssr, const unsigned char* frame, size_t size) {
     45    int bytesused = 0;
     46    return ssr->GetSSR(co, cb, v, pb, md, a, (const char*)frame, size, &bytesused);
     47  }
     48  SsrCorr::ClockOrbit* co;
     49  SsrCorr::CodeBias*   cb;
     50  SsrCorr::VTEC*       v;
     51  SsrCorr::PhaseBias*  pb;
     52  SsrCorr::MetaData*   md;
     53  SsrCorr::Antenna*    a;
     54};
     55
     56static bool sameVTEC(const SsrCorr::VTEC* v1, const SsrCorr::VTEC* v2) {
     57  if (v1->EpochTime != v2->EpochTime || v1->NumLayers != v2->NumLayers ||
     58      v1->Quality != v2->Quality) {
     59    return false;
     60  }
     61  for (unsigned l = 0; l < v1->NumLayers; l++) {
     62    const SsrCorr::VTEC::IonoLayers& l1 = v1->Layers[l];
     63    const SsrCorr::VTEC::IonoLayers& l2 = v2->Layers[l];
     64    if (l1.Height != l2.Height || l1.Degree != l2.Degree || l1.Order != l2.Order) {
     65      return false;
     66    }
     67    for (unsigned d = 0; d <= l1.Degree; d++) {
     68      for (unsigned o = 0; o <= l1.Order; o++) {
     69        if (l1.Cosinus[d][o] != l2.Cosinus[d][o] || l1.Sinus[d][o] != l2.Sinus[d][o]) {
     70          return false;
     71        }
     72      }
     73    }
     74  }
     75  return true;
     76}
     77
     78static bool sameMetaData(const SsrCorr::MetaData* m1, const SsrCorr::MetaData* m2) {
     79  if (m1->SSRIOD != m2->SSRIOD || m1->SSRProviderID != m2->SSRProviderID ||
     80      m1->SSRSolutionID != m2->SSRSolutionID || m1->NumEntries != m2->NumEntries) {
     81    return false;
     82  }
     83  for (unsigned i = 0; i < m1->NumEntries; i++) {
     84    if (memcmp(&m1->Entries[i], &m2->Entries[i], sizeof(m1->Entries[i])) != 0) {
     85      return false;
     86    }
     87  }
     88  return true;
     89}
     90
     91static int nFailed = 0;
     92
     93// Decode, print a summary, encode the result again and compare
    3594static void decode(const char* name, SsrCorr* ssr, const unsigned char* frame, size_t size) {
    36   SsrCorr::ClockOrbit* co = new SsrCorr::ClockOrbit();
    37   SsrCorr::CodeBias*   cb = new SsrCorr::CodeBias();
    38   SsrCorr::VTEC*       v  = new SsrCorr::VTEC();
    39   SsrCorr::PhaseBias*  pb = new SsrCorr::PhaseBias();
    40   SsrCorr::MetaData*   md = new SsrCorr::MetaData();
    41   SsrCorr::Antenna*    a  = new SsrCorr::Antenna();
    42   int bytesused = 0;
    43 
    44   int irc = ssr->GetSSR(co, cb, v, pb, md, a, (const char*)frame, size, &bytesused);
     95  t_target t1;
     96  int irc = t1.decode(ssr, frame, size);
    4597  printf("  %-8s irc %3d", name, irc);
    46   if (irc >= 0 && v->NumLayers) {
     98  if (irc >= 0 && t1.v->NumLayers) {
    4799    double sum = 0.0;
    48100    printf("  VTEC");
    49     for (unsigned l = 0; l < v->NumLayers; l++) {
    50       const SsrCorr::VTEC::IonoLayers& layer = v->Layers[l];
     101    for (unsigned l = 0; l < t1.v->NumLayers; l++) {
     102      const SsrCorr::VTEC::IonoLayers& layer = t1.v->Layers[l];
    51103      printf(" %u/%u", layer.Degree, layer.Order);
    52104      for (unsigned d = 0; d <= layer.Degree; d++) {
    … …  
    57109    }
    58110    printf("  sum(C-S) %.3f", sum);
     111
     112    char buffer[CLOCKORBIT_BUFFERSIZE];
     113    size_t len = ssr->MakeVTEC(t1.v, 0, buffer, sizeof(buffer));
     114    t_target t2;
     115    bool ok = len > 0 && t2.decode(ssr, (const unsigned char*)buffer, len) >= 0 && sameVTEC(t1.v, t2.v);
     116    printf("  re-encoded: %s", ok ? "identical" : "DIFFERENT");
     117    nFailed += !ok;
    59118  }
    60   if (irc >= 0 && md->NumEntries) {
    61     printf("  META %u entries, additional data", md->NumEntries);
    62     for (unsigned i = 0; i < md->NumEntries; i++) {
    63       if (md->Entries[i].additionalParameterIndicator) {
    64         printf(" %u:0x%x", md->Entries[i].additionalParameterNumBits,
    65                md->Entries[i].additionalParameterData);
     119  if (irc >= 0 && t1.md->NumEntries) {
     120    printf("  META %u entries, additional data", t1.md->NumEntries);
     121    for (unsigned i = 0; i < t1.md->NumEntries; i++) {
     122      if (t1.md->Entries[i].additionalParameterIndicator) {
     123        printf(" %u:0x%x", t1.md->Entries[i].additionalParameterNumBits,
     124               t1.md->Entries[i].additionalParameterData);
    66125      }
     126    }
     127    SsrCorrRtcmNew* rtcmNew = dynamic_cast<SsrCorrRtcmNew*>(ssr);
     128    if (rtcmNew) {
     129      char buffer[CLOCKORBIT_BUFFERSIZE];
     130      size_t len = rtcmNew->MakeMetaData(t1.md, buffer, sizeof(buffer));
     131      t_target t2;
     132      bool ok = len > 0 && t2.decode(ssr, (const unsigned char*)buffer, len) >= 0 && sameMetaData(t1.md, t2.md);
     133      printf("  re-encoded: %s", ok ? "identical" : "DIFFERENT");
     134      nFailed += !ok;
    67135    }
    68136  }
    69137  printf("\n");
    70 
    71   delete co; delete cb; delete v; delete pb; delete md; delete a;
    72138}
    73139
    … …  
    111177    ii += len + 6;
    112178  }
     179  if (nFailed) {
     180    printf("%d decoded messages differ after re-encoding\n", nFailed);
     181    return 1;
     182  }
    113183  return 0;
    114184}
Note: See TracChangeset for help on using the changeset viewer.