Coverage Report

Created: 2025-05-16 06:24

/src/ntopng/src/IEC104Stats.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 *
3
 * (C) 2013-25 - ntop.org
4
 *
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program; if not, write to the Free Software Foundation,
18
 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19
 *
20
 */
21
22
#include "ntop_includes.h"
23
#include "flow_alerts_includes.h"
24
25
/*
26
  #define DEBUG_IEC60870
27
  #define IEC60870_TRACE
28
*/
29
30
36
#define CLIENT_ALERT_SCORE 90
31
36
#define SERVER_ALERT_SCORE 10
32
33
/* *************************************** */
34
35
1.50k
IEC104Stats::IEC104Stats() {
36
1.50k
  if(trace_new_delete) ntop->getTrace()->traceEvent(TRACE_NORMAL, "[new] %s", __FILE__);
37
  
38
1.50k
  memset(&pkt_lost, 0, sizeof(pkt_lost));
39
1.50k
  last_type_i = 0;
40
1.50k
  memset(&last_i_apdu, 0, sizeof(last_i_apdu));
41
1.50k
  memset(&stats, 0, sizeof(stats));
42
1.50k
  memset(&transitions, 0, sizeof(transitions));
43
44
1.50k
  i_s_apdu = ndpi_alloc_data_analysis(32 /* sliding window side */);
45
1.50k
  tx_seq_num = rx_seq_num = 0, infobuf[0] = '\0';
46
1.50k
  invalid_command_transition_detected = false;
47
1.50k
}
48
49
/* *************************************** */
50
51
1.50k
IEC104Stats::~IEC104Stats() {
52
1.50k
  if(trace_new_delete) ntop->getTrace()->traceEvent(TRACE_NORMAL, "[new] %s", __FILE__);
53
1.50k
  ndpi_free_data_analysis(i_s_apdu, 1);
54
1.50k
}
55
56
/* *************************************** */
57
58
void IEC104Stats::processPacket(Flow *f, bool tx_direction,
59
                                const u_char *payload, u_int16_t payload_len,
60
3.98k
        const struct pcap_pkthdr *h) {
61
3.98k
  if ((payload_len >= 6) && (payload[0] == 0x68 /* IEC magic byte */)) {
62
3.92k
    u_int offset = 1 /* Skip magic byte */;
63
3.92k
    u_int64_t *allowedTypeIDs = ntop->getPrefs()->getIEC104AllowedTypeIDs();
64
3.92k
    std::unordered_map<u_int16_t, u_int32_t>::iterator it;
65
66
3.92k
    lock.wrlock(__FILE__, __LINE__);
67
68
3.92k
    if (tx_direction)
69
78
      stats.forward_msgs++;
70
3.84k
    else
71
3.84k
      stats.reverse_msgs++;
72
73
10.9k
    while ((offset+1) /* Skip START byte */ < payload_len) {
74
      /* https://infosys.beckhoff.com/english.php?content=../content/1033/tcplclibiec870_5_104/html/tcplclibiec870_5_104_objref_overview.htm&id
75
       */
76
10.6k
      u_int8_t len = payload[offset];
77
10.6k
      u_int8_t pdu_type = ((payload[offset + 1] & 0x01) == 0) ? 0 : (payload[offset + 1] & 0x03);
78
79
#ifdef DEBUG_IEC60870
80
      ntop->getTrace()->traceEvent(TRACE_WARNING, "[%s] %02X %02X %02X %02X",
81
                                   __FUNCTION__, payload[offset - 1],
82
                                   payload[offset], payload[offset + 1],
83
                                   payload[offset + 2]);
84
#endif
85
86
#ifdef DEBUG_IEC60870
87
      ntop->getTrace()->traceEvent(
88
          TRACE_WARNING, "[%s] A-PDU Len %u/%u [pdu_type: %u][magic: %02X]",
89
          __FUNCTION__, len, payload_len, pdu_type, payload[offset - 1]);
90
#endif
91
92
10.6k
      if (len == 0) break; /* Something went wrong */
93
94
10.6k
      switch (pdu_type) {
95
987
        case 0x03: /* U */
96
987
  {
97
987
          u_int8_t u_type = (payload[offset + 1] & 0xFC) >> 2;
98
987
          const char *u_type_str;
99
100
#ifdef IEC60870_TRACE
101
          ntop->getTrace()->traceEvent(TRACE_NORMAL, "A-PDU U-%u",
102
                                       (payload[offset + 1] & 0xFC) >> 2);
103
#endif
104
          /* No rx and tx to be updated */
105
987
          stats.type_u++;
106
107
987
          switch (u_type) {
108
35
            case 0x01:
109
35
              u_type_str = "STARTDT act";
110
35
              break;
111
112
74
            case 0x02:
113
74
              u_type_str = "STARTDT con";
114
74
              break;
115
116
18
            case 0x04:
117
18
              u_type_str = "STOPDT act";
118
18
              break;
119
120
77
            case 0x08:
121
77
              u_type_str = "STOPDT con";
122
77
              break;
123
124
51
            case 0x10:
125
51
              u_type_str = "TESTFR act";
126
51
              break;
127
128
20
            case 0x20:
129
20
              u_type_str = "TESTFR con";
130
20
              break;
131
132
712
            default:
133
712
              u_type_str = "???";
134
712
              break;
135
987
          }
136
137
987
          snprintf(infobuf, sizeof(infobuf) - 1, "%s U (%s)",
138
987
                   tx_direction ? "->" : "<-", u_type_str);
139
987
        }
140
0
  break;
141
142
953
        case 0x01: /* S */
143
953
    if((offset+4) < payload_len) {
144
945
      if (len >= 4) {
145
712
        u_int16_t rx = ((((u_int16_t)payload[offset + 4]) << 8) + payload[offset + 3]) >> 1;
146
147
712
        if (last_i_apdu.tv_sec != 0) {
148
337
    float ms = Utils::msTimevalDiff(&h->ts, &last_i_apdu);
149
150
#ifdef IEC60870_TRACE
151
    ntop->getTrace()->traceEvent(
152
                  TRACE_NORMAL,
153
                  "A-PDU S [last I-TX: %u][S RX ack: %u][tdiff: %.2f msec]",
154
                  tx_seq_num, rx, ms);
155
#endif
156
    /*
157
      In theory if all is in good shape
158
      (rx + 1) == tx_seq_num
159
    */
160
161
337
    ndpi_data_add_value(i_s_apdu, ms);
162
337
        }
163
164
        /* No rx and tx to be updated */
165
712
        snprintf(infobuf, sizeof(infobuf) - 1, "%s S, RX %u",
166
712
           tx_direction ? "->" : "<-", rx);
167
712
      }
168
169
945
      stats.type_s++;
170
945
    }
171
953
          break;
172
10.6k
      }
173
174
10.6k
      if (pdu_type != 0x0 /* Type I */) {
175
1.94k
        offset += len + 2;
176
1.94k
        stats.type_other++;
177
1.94k
        continue;
178
1.94k
      }
179
180
      /* From now on, only Type I packets are processed */
181
8.71k
      memcpy(&last_i_apdu, &h->ts, sizeof(struct timeval));
182
8.71k
      stats.type_i++;
183
184
8.71k
      if(((offset + 6) < payload_len) && (len >= 6 /* Ignore 4 bytes APDUs */)) {
185
8.66k
        u_int16_t rx_value, tx_value;
186
8.66k
        bool initial_run = ((rx_seq_num == 0) && (tx_seq_num == 0)) ? true : false;
187
188
8.66k
        tx_value = ((((u_int16_t)payload[offset + 2]) << 8) + payload[offset + 1]) >> 1;
189
8.66k
        rx_value = ((((u_int16_t)payload[offset + 4]) << 8) + payload[offset + 3]) >> 1;
190
191
8.66k
        if (!tx_direction) {
192
          /* Counters are swapped */
193
8.55k
          u_int16_t v = rx_value;
194
195
8.55k
          rx_value = tx_value;
196
8.55k
          tx_value = v;
197
8.55k
        }
198
199
8.66k
        if ((tx_value == tx_seq_num) && (rx_value == rx_seq_num)) {
200
42
          stats.retransmitted_msgs++;
201
42
          lock.unlock(__FILE__, __LINE__);
202
42
          return;
203
42
        }
204
205
8.62k
        if (!initial_run) {
206
7.27k
          u_int32_t diff = abs(tx_value - (tx_seq_num + 1));
207
208
          /* Check for id reset (16 bit only) */
209
7.27k
          if (diff != 32768) pkt_lost.tx += diff;
210
7.27k
        }
211
8.62k
        tx_seq_num = tx_value;
212
213
8.62k
        if (!tx_direction) {
214
8.53k
          if (!initial_run) {
215
7.20k
            u_int32_t diff = abs(rx_value - rx_seq_num);
216
217
            /* Check for id reset (16 bit only) */
218
7.20k
            if (diff != 32768) pkt_lost.rx += diff;
219
7.20k
          }
220
221
8.53k
          rx_value++; /* The next RX will be increased by 1 */
222
8.53k
        } else {
223
90
          if (!initial_run) {
224
63
            u_int32_t diff = abs(rx_value - rx_seq_num);
225
226
            /* Check for id reset (16 bit only) */
227
63
            if (diff != 32768) pkt_lost.rx += diff;
228
63
          }
229
90
        }
230
8.62k
        rx_seq_num = rx_value;
231
232
        /* Skip magic(1), len(1), type/TX(2), RX(2) = 6 */
233
8.62k
        len -= 6 /* Skip magic and len */,
234
8.62k
    offset += 5 /* magic already skept */;
235
236
8.62k
        if (payload_len >= (offset + len)) {
237
8.53k
          u_int8_t type_id = payload[offset];
238
8.53k
          u_int8_t cause_tx = payload[offset + 1] & 0x3F;
239
8.53k
          u_int8_t negative =
240
8.53k
              ((payload[offset + 1] & 0x40) == 0x40) ? true : false;
241
8.53k
          u_int16_t asdu;
242
8.53k
          u_int64_t bit;
243
8.53k
          bool unexpected_typeid_alerted = false;
244
245
8.53k
          offset += len + 2 /* magic and len */;
246
247
8.53k
          if((len >= 6) && ((offset + 6) <= payload_len))
248
3.49k
            asdu = /* ntohs */ (*((u_int16_t *)&payload[4 + offset]));
249
5.03k
          else
250
5.03k
            asdu = 0;
251
252
#ifdef DEBUG_IEC60870
253
          ntop->getTrace()->traceEvent(
254
              TRACE_WARNING, "[%s] TypeId %u [offset %u/%u]", __FUNCTION__,
255
              type_id, offset, payload_len);
256
#endif
257
258
#ifdef IEC60870_TRACE
259
          ntop->getTrace()->traceEvent(
260
              TRACE_NORMAL,
261
              "[%s] A-PDU I-%-3u [rx: %u][tx: %u][lost rx/tx: %u/%u]",
262
              tx_direction ? "->" : "<-", type_id, rx_seq_num, tx_seq_num,
263
              pkt_lost.rx, pkt_lost.tx);
264
#endif
265
266
8.53k
          snprintf(infobuf, sizeof(infobuf) - 1, "%s I, RX %u, TX %u",
267
8.53k
                   tx_direction ? "->" : "<-", rx_seq_num, tx_seq_num);
268
269
8.53k
          if (!initial_run) {
270
7.21k
            u_int32_t transition = (last_type_i << 8) + type_id;
271
272
7.21k
            it = type_i_transitions.find(transition);
273
274
7.21k
            if (it == type_i_transitions.end()) {
275
3.86k
              if (f->get_duration() > ntop->getPrefs()->getIEC60870LearingPeriod()) {
276
377
                FlowAlert *alert = NULL;
277
377
                u_int16_t c_score = 50, s_score = 10;
278
279
#ifdef IEC60870_TRACE
280
                ntop->getTrace()->traceEvent(TRACE_NORMAL,
281
                                             "Found new transition %u -> %u",
282
                                             last_type_i, type_id);
283
#endif
284
377
                char key[128], rsp[64];
285
377
                snprintf(key, sizeof(key), CHECKS_IEC_INVALID_TRANSITION);
286
287
377
                if ((!ntop->getRedis()->get(key, rsp, sizeof(rsp))) &&
288
377
                    ((rsp[0] != '\0') && (!strcmp(rsp, "1"))))
289
0
                  alert = new IECInvalidTransitionAlert(NULL, f,
290
0
              (struct timeval*)&h->ts,
291
0
                                                        last_type_i, type_id);
292
293
377
                if (alert) {
294
0
                  alert->setCliSrvScores(c_score, s_score);
295
0
                  f->triggerAlert(alert, true);
296
0
                }
297
298
377
                type_i_transitions[transition] = 2; /* Post Learning */
299
377
              } else
300
3.49k
                type_i_transitions[transition] = 1; /* During Learning */
301
3.86k
            } else
302
3.34k
              type_i_transitions[transition] = it->second + 1;
303
7.21k
          }
304
305
8.53k
          if (!initial_run) {
306
7.21k
            if (isMonitoringTypeId(last_type_i) && isMonitoringTypeId(type_id))
307
2.32k
              transitions.m_to_m++;
308
4.88k
            else if (isMonitoringTypeId(last_type_i) &&
309
4.88k
                     isCommandTypeId(type_id))
310
462
              transitions.m_to_c++;
311
4.42k
            else if (isCommandTypeId(last_type_i) &&
312
4.42k
                     isMonitoringTypeId(type_id))
313
469
              transitions.c_to_m++;
314
3.95k
            else if (isCommandTypeId(last_type_i) && isCommandTypeId(type_id))
315
922
              transitions.c_to_c++;
316
317
7.21k
            if ((invalid_command_transition_detected == false) &&
318
7.21k
                ((transitions.m_to_c > 20) || (transitions.c_to_m > 20) ||
319
6.93k
                 (transitions.c_to_c > 5))) {
320
              /* https://github.com/ntop/ntopng/issues/6598 */
321
36
              FlowAlert *alert = NULL;
322
36
              u_int16_t c_score = CLIENT_ALERT_SCORE,
323
36
                        s_score = SERVER_ALERT_SCORE;
324
325
36
              char key[128], rsp[64];
326
36
              snprintf(key, sizeof(key), CHECKS_IEC_INVALID_COMMAND_TRANSITION);
327
328
36
              if ((!ntop->getRedis()->get(key, rsp, sizeof(rsp))) &&
329
36
                  ((rsp[0] != '\0') && (!strcmp(rsp, "1"))))
330
0
                alert = new IECInvalidCommandTransitionAlert(NULL, f,
331
0
                   (struct timeval*)&h->ts, transitions.m_to_c,
332
0
                   transitions.c_to_m, transitions.c_to_c);
333
334
36
              if (alert) {
335
0
                alert->setCliSrvScores(c_score, s_score);
336
0
                f->triggerAlert(alert, true);
337
0
              }
338
339
              // ntop->getTrace()->traceEvent(TRACE_WARNING, "*** INVALID
340
              // TRANSITION %u -> %u", last_type_i, type_id);
341
342
36
              invalid_command_transition_detected = true;
343
36
            }
344
7.21k
          }
345
346
8.53k
          last_type_i = type_id;
347
348
8.53k
          it = typeid_uses.find(type_id);
349
350
8.53k
          if (it == typeid_uses.end())
351
3.59k
            typeid_uses[type_id] = 1;
352
4.93k
          else
353
4.93k
            typeid_uses[type_id] = it->second + 1;
354
355
8.53k
          if (type_id < 64) {
356
5.26k
            bit = ((u_int64_t)1) << type_id;
357
5.26k
            if ((allowedTypeIDs[0] & bit) == 0)
358
0
              unexpected_typeid_alerted = true;
359
5.26k
          } else if (type_id < 128) {
360
2.16k
            bit = ((u_int64_t)1) << (type_id - 64);
361
362
2.16k
            if ((allowedTypeIDs[1] & bit) == 0)
363
0
              unexpected_typeid_alerted = true;
364
2.16k
          }
365
366
8.53k
          if (unexpected_typeid_alerted) {
367
0
            FlowAlert *alert = NULL;
368
0
            u_int16_t c_score = CLIENT_ALERT_SCORE,
369
0
                      s_score = SERVER_ALERT_SCORE;
370
371
0
            char key[128], rsp[64];
372
0
            snprintf(key, sizeof(key), CHECKS_IEC_UNEXPECTED_TYPE_ID);
373
374
0
            if ((!ntop->getRedis()->get(key, rsp, sizeof(rsp))) &&
375
0
                ((rsp[0] != '\0') && (!strcmp(rsp, "1"))))
376
0
              alert = new IECUnexpectedTypeIdAlert(NULL, f, type_id, asdu,
377
0
                                                   cause_tx, negative);
378
379
0
            if (alert) {
380
0
              alert->setCliSrvScores(c_score, s_score);
381
0
              f->triggerAlert(alert, true);
382
0
            }
383
0
          } /* unexpected_typeid_alerted */
384
          /* Discard typeIds 127..255 */
385
8.53k
        } else /* payload_len < len */
386
94
          break;
387
8.62k
      } else {
388
        // ntop->getTrace()->traceEvent(TRACE_WARNING, "*** short APDUs");
389
49
        break;
390
49
      }
391
      
392
8.53k
      if((offset < payload_len) && (payload[offset] == 0x68 /* IEC magic byte */))
393
5.12k
        offset += 1; /* We skip the initial magic byte */
394
3.40k
      else {
395
#ifdef DEBUG_IEC60870
396
        ntop->getTrace()->traceEvent(
397
            TRACE_WARNING, "Skipping IEC entry: no magic byte @ offset %u",
398
            offset);
399
#endif
400
3.40k
        break;
401
3.40k
      }
402
8.53k
    } /* while */
403
404
3.88k
    lock.unlock(__FILE__, __LINE__);
405
3.88k
  }
406
3.98k
}
407
408
/* *************************************** */
409
410
0
void IEC104Stats::lua(lua_State *vm) {
411
0
  lua_newtable(vm);
412
413
0
  lock.rdlock(__FILE__, __LINE__);
414
415
  /* *************************** */
416
417
0
  lua_newtable(vm);
418
419
0
  for (std::unordered_map<u_int16_t, u_int32_t>::iterator it = typeid_uses.begin();
420
0
       it != typeid_uses.end(); ++it) {
421
0
    char buf[8];
422
423
0
    snprintf(buf, sizeof(buf), "%u", it->first);
424
0
    lua_push_int32_table_entry(vm, buf, it->second);
425
0
  }
426
427
0
  lua_pushstring(vm, "typeid");
428
0
  lua_insert(vm, -2);
429
0
  lua_settable(vm, -3);
430
431
  /* *************************** */
432
433
0
  lua_newtable(vm);
434
435
0
  for (std::unordered_map<u_int16_t, u_int32_t>::iterator it =
436
0
           type_i_transitions.begin();
437
0
       it != type_i_transitions.end(); ++it) {
438
0
    char buf[8];
439
440
0
    snprintf(buf, sizeof(buf), "%u,%u", (it->first >> 8), it->first & 0xFF);
441
0
    lua_push_int32_table_entry(vm, buf, it->second);
442
0
  }
443
444
0
  lua_pushstring(vm, "typeid_transitions");
445
0
  lua_insert(vm, -2);
446
0
  lua_settable(vm, -3);
447
448
0
  lock.unlock(__FILE__, __LINE__);
449
450
  /* *************************** */
451
452
0
  lua_newtable(vm);
453
0
  lua_push_int32_table_entry(vm, "type_i", stats.type_i);
454
0
  lua_push_int32_table_entry(vm, "type_s", stats.type_s);
455
0
  lua_push_int32_table_entry(vm, "type_u", stats.type_u);
456
0
  lua_push_int32_table_entry(vm, "type_other", stats.type_other);
457
0
  lua_push_int32_table_entry(vm, "forward_msgs", stats.forward_msgs);
458
0
  lua_push_int32_table_entry(vm, "reverse_msgs", stats.reverse_msgs);
459
0
  lua_push_int32_table_entry(vm, "retransmitted_msgs",
460
0
                             stats.retransmitted_msgs);
461
0
  lua_pushstring(vm, "stats");
462
0
  lua_insert(vm, -2);
463
0
  lua_settable(vm, -3);
464
465
  /* *************************** */
466
467
0
  lua_newtable(vm);
468
0
  lua_push_int32_table_entry(vm, "rx", pkt_lost.rx);
469
0
  lua_push_int32_table_entry(vm, "tx", pkt_lost.tx);
470
0
  lua_pushstring(vm, "pkt_lost");
471
0
  lua_insert(vm, -2);
472
0
  lua_settable(vm, -3);
473
474
  /* *************************** */
475
476
0
  lua_newtable(vm);
477
0
  lua_push_float_table_entry(vm, "average", ndpi_data_average(i_s_apdu));
478
0
  lua_push_float_table_entry(vm, "stddev", ndpi_data_stddev(i_s_apdu));
479
0
  lua_pushstring(vm, "ack_time");
480
0
  lua_insert(vm, -2);
481
0
  lua_settable(vm, -3);
482
483
  /* *************************** */
484
485
0
  lua_pushstring(vm, "iec104");
486
0
  lua_insert(vm, -2);
487
0
  lua_settable(vm, -3);
488
0
}
489
490
/* *************************************** */
491
492
0
std::string IEC104Stats::getFlowInfo() {
493
0
  lock.rdlock(__FILE__, __LINE__);
494
0
  std::string info_field = std::string(infobuf);
495
0
  lock.unlock(__FILE__, __LINE__);
496
497
0
  return info_field;
498
0
}
499
500
/* *************************************** */
501
502
17.5k
bool IEC104Stats::isMonitoringTypeId(u_int16_t tid) {
503
17.5k
  return (((tid <= 40) || (tid == 70)) ? true : false);
504
17.5k
}
505
506
/* *************************************** */
507
508
10.9k
bool IEC104Stats::isCommandTypeId(u_int16_t tid) {
509
10.9k
  if (((tid >= 45) && (tid <= 64)) || ((tid >= 100) && (tid <= 107)))
510
5.17k
    return (true);
511
5.80k
  else
512
5.80k
    return (false);
513
10.9k
}
514
515
/* *************************************** */