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