1 /** @file
2
3 tcpinfo: A plugin to log TCP session information.
4
5 @section license License
6
7 Licensed to the Apache Software Foundation (ASF) under one
8 or more contributor license agreements. See the NOTICE file
9 distributed with this work for additional information
10 regarding copyright ownership. The ASF licenses this file
11 to you under the Apache License, Version 2.0 (the
12 "License"); you may not use this file except in compliance
13 with the License. You may obtain a copy of the License at
14
15 http://www.apache.org/licenses/LICENSE-2.0
16
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
22 */
23
24 #include "tscore/ink_config.h"
25
26 #include <cstdio>
27 #include <cstdlib>
28 #include <memory>
29 #include <ts/ts.h>
30 #include <unistd.h>
31 #include <netinet/in.h>
32 // This is a bit of a hack, to get the more linux specific tcp_info struct ...
33 #if HAVE_STRUCT_LINUX_TCP_INFO
34 #include <linux/tcp.h>
35 #elif HAVE_NETINET_IN_H
36 #include <netinet/tcp.h>
37 #endif
38 #include <sys/types.h>
39 #include <sys/stat.h>
40 #include <getopt.h>
41 #include <fcntl.h>
42 #include <climits>
43 #include <cstring>
44 #include <cerrno>
45 #include <sys/time.h>
46 #include <arpa/inet.h>
47
48 #include "tscore/ParseRules.h"
49
50 #if defined(TCP_INFO) && defined(HAVE_STRUCT_TCP_INFO)
51 #define TCPI_PLUGIN_SUPPORTED 1
52 #endif
53
54 #define TCPI_HOOK_SSN_START 0x01u
55 #define TCPI_HOOK_TXN_START 0x02u
56 #define TCPI_HOOK_SEND_RESPONSE 0x04u
57 #define TCPI_HOOK_TXN_CLOSE 0x10u
58
59 // Log format headers. These are emitted once at the start of a log file. Note that we
60 // carefully order the fields so the field ordering is compatible. This lets you change
61 // the verbosity without breaking a parser that is moderately robust.
62 static const char *tcpi_headers[] = {"timestamp event client server rtt",
63 "timestamp event client server rtt rttvar last_sent last_recv snd_cwnd "
64 "snd_ssthresh rcv_ssthresh unacked sacked lost retrans fackets all_retrans"
65 // Additional information from linux's linux/tcp.h appended here
66 #if HAVE_STRUCT_LINUX_TCP_INFO
67 " data_segs_in data_segs_out"
68 #endif
69 };
70
71 struct Config {
72 int sample = 1000;
73 unsigned int log_level = 1;
74 TSTextLogObject log = nullptr;
75
~ConfigConfig76 ~Config()
77 {
78 if (log) {
79 TSTextLogObjectDestroy(log);
80 }
81 }
82 };
83
84 union const_sockaddr_ptr {
85 const struct sockaddr *sa;
86 const struct sockaddr_in *in;
87 const struct sockaddr_in6 *in6;
88
89 const void *
addr() const90 addr() const
91 {
92 switch (sa->sa_family) {
93 case AF_INET:
94 return &(in->sin_addr);
95 case AF_INET6:
96 return &(in6->sin6_addr);
97 default:
98 return nullptr;
99 }
100 }
101 };
102
103 #if TCPI_PLUGIN_SUPPORTED
104
105 static void
log_tcp_info(Config * config,const char * event_name,TSHttpSsn ssnp)106 log_tcp_info(Config *config, const char *event_name, TSHttpSsn ssnp)
107 {
108 char client_str[INET6_ADDRSTRLEN];
109 char server_str[INET6_ADDRSTRLEN];
110 const_sockaddr_ptr client_addr;
111 const_sockaddr_ptr server_addr;
112
113 struct tcp_info info;
114 socklen_t tcp_info_len = sizeof(info);
115 int fd;
116
117 TSReleaseAssert(config->log != nullptr);
118
119 if (ssnp != nullptr && (TSHttpSsnClientFdGet(ssnp, &fd) != TS_SUCCESS || fd <= 0)) {
120 TSDebug("tcpinfo", "error getting the client socket fd from ssn");
121 return;
122 }
123 if (ssnp == nullptr) {
124 TSDebug("tcpinfo", "ssn is not specified");
125 return;
126 }
127
128 if (getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, &tcp_info_len) != 0) {
129 TSDebug("tcpinfo", "getsockopt(%d, TCP_INFO) failed: %s", fd, strerror(errno));
130 return;
131 }
132
133 client_addr.sa = TSHttpSsnClientAddrGet(ssnp);
134 server_addr.sa = TSHttpSsnIncomingAddrGet(ssnp);
135
136 if (client_addr.sa == nullptr || server_addr.sa == nullptr) {
137 return;
138 }
139
140 // convert ip to string
141 inet_ntop(client_addr.sa->sa_family, client_addr.addr(), client_str, sizeof(client_str));
142 inet_ntop(server_addr.sa->sa_family, server_addr.addr(), server_str, sizeof(server_str));
143
144 TSReturnCode ret;
145
146 if (config->log_level == 2) {
147 #if !defined(freebsd) || defined(__GLIBC__)
148 #if HAVE_STRUCT_LINUX_TCP_INFO
149 ret = TSTextLogObjectWrite(config->log, "%s %s %s %u %u %u %u %u %u %u %u %u %u %u %u %u %u %u", event_name, client_str,
150 server_str, info.tcpi_rtt, info.tcpi_rttvar, info.tcpi_last_data_sent, info.tcpi_last_data_recv,
151 info.tcpi_snd_cwnd, info.tcpi_snd_ssthresh, info.tcpi_rcv_ssthresh, info.tcpi_unacked,
152 info.tcpi_sacked, info.tcpi_lost, info.tcpi_retrans, info.tcpi_fackets, info.tcpi_total_retrans,
153 info.tcpi_data_segs_in, info.tcpi_data_segs_out);
154 #else
155 ret = TSTextLogObjectWrite(config->log, "%s %s %s %u %u %u %u %u %u %u %u %u %u %u %u %u", event_name, client_str, server_str,
156 info.tcpi_rtt, info.tcpi_rttvar, info.tcpi_last_data_sent, info.tcpi_last_data_recv,
157 info.tcpi_snd_cwnd, info.tcpi_snd_ssthresh, info.tcpi_rcv_ssthresh, info.tcpi_unacked,
158 info.tcpi_sacked, info.tcpi_lost, info.tcpi_retrans, info.tcpi_fackets, info.tcpi_total_retrans);
159 #endif
160 #else
161 // E.g. FreeBSD and macOS
162 ret = TSTextLogObjectWrite(config->log, "%s %s %s %u %u %u %u %u %u %u %u %u %u %u %u %u", event_name, client_str, server_str,
163 info.tcpi_rtt, info.tcpi_rttvar, info.__tcpi_last_data_sent, info.tcpi_last_data_recv,
164 info.tcpi_snd_cwnd, info.tcpi_snd_ssthresh, info.__tcpi_rcv_ssthresh, info.__tcpi_unacked,
165 info.__tcpi_sacked, info.__tcpi_lost, info.__tcpi_retrans, info.__tcpi_fackets,
166 info.tcpi_snd_rexmitpack);
167 #endif
168 } else {
169 ret = TSTextLogObjectWrite(config->log, "%s %s %s %u", event_name, client_str, server_str, info.tcpi_rtt);
170 }
171
172 if (ret != TS_SUCCESS) {
173 // ToDo: This could be due to a failure, or logs full. Should we consider
174 // closing / reopening the log? If so, how often do we do that ?
175 }
176 }
177
178 #else /* TCPI_PLUGIN_SUPPORTED */
179
180 static void
log_tcp_info(Config *,const char *,TSHttpSsn)181 log_tcp_info(Config * /* config */, const char * /* event_name */, TSHttpSsn /* ssnp */)
182 {
183 return; // TCP metrics not supported.
184 }
185
186 #endif /* TCPI_PLUGIN_SUPPORTED */
187
188 static int
tcp_info_hook(TSCont contp,TSEvent event,void * edata)189 tcp_info_hook(TSCont contp, TSEvent event, void *edata)
190 {
191 TSHttpSsn ssnp = nullptr;
192 TSHttpTxn txnp = nullptr;
193 int random = 0;
194 Config *config = static_cast<Config *>(TSContDataGet(contp));
195
196 const char *event_name;
197 switch (event) {
198 case TS_EVENT_HTTP_SSN_START:
199 ssnp = static_cast<TSHttpSsn>(edata);
200 event_name = "ssn_start";
201 break;
202 case TS_EVENT_HTTP_TXN_START:
203 txnp = static_cast<TSHttpTxn>(edata);
204 ssnp = TSHttpTxnSsnGet(txnp);
205 event_name = "txn_start";
206 break;
207 case TS_EVENT_HTTP_TXN_CLOSE:
208 txnp = static_cast<TSHttpTxn>(edata);
209 ssnp = TSHttpTxnSsnGet(txnp);
210 event_name = "txn_close";
211 break;
212 case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
213 txnp = static_cast<TSHttpTxn>(edata);
214 ssnp = TSHttpTxnSsnGet(txnp);
215 event_name = "send_resp_hdr";
216 break;
217 default:
218 return 0;
219 }
220
221 TSDebug("tcpinfo", "logging hook called for %s (%s) with log object %p", TSHttpEventNameLookup(event), event_name, config->log);
222
223 if (config->log == nullptr) {
224 goto done;
225 }
226
227 // Don't try to sample internal requests. TCP metrics for loopback are not interesting.
228 if (TSHttpSsnIsInternal(ssnp)) {
229 goto done;
230 }
231
232 // no need to run rand if we are always going log (100%)
233 if (config->sample < 1000) {
234 // coverity[dont_call]
235 random = rand() % 1000;
236 TSDebug("tcpinfo", "random: %d, config->sample: %d", random, config->sample);
237 }
238
239 if (random < config->sample) {
240 TSDebug("tcpinfo", "sampling TCP metrics for %s event", event_name);
241 log_tcp_info(config, event_name, ssnp);
242 }
243
244 done:
245 if (txnp != nullptr) {
246 TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
247 } else if (ssnp != nullptr) {
248 TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
249 }
250
251 return TS_EVENT_NONE;
252 }
253
254 static bool
parse_unsigned(const char * str,unsigned long & lval)255 parse_unsigned(const char *str, unsigned long &lval)
256 {
257 char *end = nullptr;
258
259 if (*str == '\0') {
260 return false;
261 }
262
263 lval = strtoul(str, &end, 0 /* base */);
264 if (end == str) {
265 // No valid characters.
266 return false;
267 }
268
269 if (end && *end != '\0') {
270 // Not all characters consumed.
271 return false;
272 }
273
274 return true;
275 }
276
277 // Parse a comma-separated list of hook names into a hook bitmask.
278 static unsigned
parse_hook_list(const char * hook_list)279 parse_hook_list(const char *hook_list)
280 {
281 unsigned mask = 0;
282 char *tok;
283 char *str;
284 char *last;
285
286 const struct hookmask {
287 const char *name;
288 unsigned mask;
289 } hooks[] = {{"ssn_start", TCPI_HOOK_SSN_START},
290 {"txn_start", TCPI_HOOK_TXN_START},
291 {"send_resp_hdr", TCPI_HOOK_SEND_RESPONSE},
292 {"txn_close", TCPI_HOOK_TXN_CLOSE},
293 {nullptr, 0u}};
294
295 str = TSstrdup(hook_list);
296
297 for (tok = strtok_r(str, ",", &last); tok; tok = strtok_r(nullptr, ",", &last)) {
298 bool match = false;
299
300 for (const struct hookmask *m = hooks; m->name != nullptr; ++m) {
301 if (strcmp(m->name, tok) == 0) {
302 mask |= m->mask;
303 match = true;
304 break;
305 }
306 }
307
308 if (!match) {
309 TSError("[tcpinfo] invalid hook name '%s'", tok);
310 }
311 }
312
313 TSfree(str);
314 return mask;
315 }
316
317 void
TSPluginInit(int argc,const char * argv[])318 TSPluginInit(int argc, const char *argv[])
319 {
320 static const char usage[] = "tcpinfo.so [--log-file=PATH] [--log-level=LEVEL] [--hooks=LIST] [--sample-rate=COUNT] "
321 "[--rolling-enabled=VALUE] [--rolling-offset-hr=HOUR] [--rolling-interval-sec=SECONDS] "
322 "[--rolling-size=MB]";
323 static const struct option longopts[] = {
324 {const_cast<char *>("sample-rate"), required_argument, nullptr, 'r'},
325 {const_cast<char *>("log-file"), required_argument, nullptr, 'f'},
326 {const_cast<char *>("log-level"), required_argument, nullptr, 'l'},
327 {const_cast<char *>("hooks"), required_argument, nullptr, 'h'},
328 {const_cast<char *>("rolling-enabled"), required_argument, nullptr, 'e'},
329 {const_cast<char *>("rolling-offset-hr"), required_argument, nullptr, 'H'},
330 {const_cast<char *>("rolling-interval-sec"), required_argument, nullptr, 'S'},
331 {const_cast<char *>("rolling-size"), required_argument, nullptr, 'M'},
332 {nullptr, 0, nullptr, 0},
333 };
334
335 TSPluginRegistrationInfo info;
336 auto config = std::make_unique<Config>();
337 const char *filename = "tcpinfo";
338 TSCont cont;
339 unsigned int hooks = 0;
340 int rolling_enabled = -1;
341 unsigned int rolling_interval_sec = 86400;
342 unsigned int rolling_offset_hr = 0;
343 unsigned int rolling_size = 1024;
344 unsigned int i = 0;
345 char *endptr;
346
347 info.plugin_name = (char *)"tcpinfo";
348 info.vendor_name = (char *)"Apache Software Foundation";
349 info.support_email = (char *)"dev@trafficserver.apache.org";
350
351 if (TSPluginRegister(&info) != TS_SUCCESS) {
352 TSError("[tcpinfo] plugin registration failed");
353 }
354
355 for (;;) {
356 unsigned long lval;
357
358 switch (getopt_long(argc, const_cast<char *const *>(argv), "r:f:l:h:e:H:S:M:", longopts, nullptr)) {
359 case 'r':
360 if (parse_unsigned(optarg, lval)) {
361 config->sample = atoi(optarg);
362 } else {
363 TSError("[tcpinfo] invalid sample rate '%s'", optarg);
364 }
365 break;
366 case 'f':
367 filename = optarg;
368 break;
369 case 'l':
370 if (parse_unsigned(optarg, lval) && (lval <= countof(tcpi_headers))) {
371 config->log_level = lval;
372 } else {
373 TSError("[tcpinfo] invalid log level '%s'", optarg);
374 }
375 break;
376 case 'h':
377 hooks = parse_hook_list(optarg);
378 break;
379 case 'e':
380 i = strtoul(optarg, &endptr, 10);
381 if (*endptr != '\0' || i > 3) {
382 TSError("[tcpinfo] invalid rolling-enabled argument, '%s', using the system's proxy.config.log.rolling_enabled value",
383 optarg);
384 } else {
385 rolling_enabled = i;
386 }
387 break;
388 case 'H':
389 i = strtoul(optarg, &endptr, 10);
390 if (*endptr != '\0' || i > 23) {
391 TSError("[tcpinfo] invalid rolling-offset-hr argument, '%s', using default of %d", optarg, rolling_offset_hr);
392 } else {
393 rolling_offset_hr = i;
394 }
395 break;
396 case 'S':
397 i = strtoul(optarg, &endptr, 10);
398 if (*endptr != '\0' || i < 60 || i > 86400) {
399 TSError("[tcpinfo] invalid rolling-interval-sec argument, '%s', using default of %d", optarg, rolling_interval_sec);
400 } else {
401 rolling_interval_sec = i;
402 }
403 break;
404 case 'M':
405 i = ink_atoui(optarg);
406 if (i < 10) {
407 TSError("[tcpinfo] invalid rolling-size argument, '%s', using default of %d", optarg, rolling_size);
408 } else {
409 rolling_size = i;
410 }
411 break;
412 case -1:
413 goto init;
414 default:
415 TSError("[tcpinfo] usage: %s", usage);
416 }
417 }
418
419 init:
420
421 #if !TCPI_PLUGIN_SUPPORTED
422 TSError("[tcpinfo] TCP metrics are not supported on this platform");
423 #endif
424
425 TSDebug("tcpinfo", "sample: %d", config->sample);
426 TSDebug("tcpinfo", "log filename: %s", filename);
427 TSDebug("tcpinfo", "log_level: %u", config->log_level);
428 TSDebug("tcpinfo", "rolling_enabled: %d", rolling_enabled);
429 TSDebug("tcpinfo", "hook mask: 0x%x", hooks);
430
431 if (TSTextLogObjectCreate(filename, TS_LOG_MODE_ADD_TIMESTAMP, &config->log) != TS_SUCCESS) {
432 TSError("[tcpinfo] failed to create log file '%s'", filename);
433 return;
434 }
435 if (rolling_enabled == -1) {
436 // The user either did not provide a value or the value they provided was
437 // invalid.
438 TSDebug("tcpinfo", "Using system default value of proxy.config.log.rolling_enabled ");
439 } else {
440 if (TSTextLogObjectRollingEnabledSet(config->log, rolling_enabled) != TS_SUCCESS) {
441 TSError("[tcpinfo] failed to enable log file rolling to: '%d'", rolling_enabled);
442 return;
443 }
444 }
445 TSTextLogObjectRollingIntervalSecSet(config->log, rolling_interval_sec);
446 TSTextLogObjectRollingOffsetHrSet(config->log, rolling_offset_hr);
447 TSTextLogObjectRollingSizeMbSet(config->log, rolling_size);
448
449 TSTextLogObjectHeaderSet(config->log, tcpi_headers[config->log_level - 1]);
450
451 cont = TSContCreate(tcp_info_hook, nullptr);
452 TSContDataSet(cont, config.release());
453
454 if (hooks & TCPI_HOOK_SSN_START) {
455 TSHttpHookAdd(TS_HTTP_SSN_START_HOOK, cont);
456 TSDebug("tcpinfo", "added hook to the start of the TCP connection");
457 }
458
459 if (hooks & TCPI_HOOK_TXN_START) {
460 TSHttpHookAdd(TS_HTTP_TXN_START_HOOK, cont);
461 TSDebug("tcpinfo", "added hook to the start of the transaction");
462 }
463
464 if (hooks & TCPI_HOOK_SEND_RESPONSE) {
465 TSHttpHookAdd(TS_HTTP_SEND_RESPONSE_HDR_HOOK, cont);
466 TSDebug("tcpinfo", "added hook to the sending of the headers");
467 }
468
469 if (hooks & TCPI_HOOK_TXN_CLOSE) {
470 TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, cont);
471 TSDebug("tcpinfo", "added hook to the close of the transaction");
472 }
473 }
474