Introduction
Recently, we needed a way to monitor a remote system's IPv4-TCP connection table without having to periodically log on to the system and run the "netstat" command. The solution that I came up with, a lightweight netstat like server, provides real-time connection monitoring viewable from any internet browser. The project is a nice example of how to add web-interfaces to applications that only have text UIs.
Background
We used the Microsoft function GetTcpTable
for gathering connection data. A Microsoft example, which illustrated how to use the function, listed below, provided a nice starting point.
1 2 #include <winsock2.h>
3 #include <ws2tcpip.h>
4 #include <iphlpapi.h>
5 #include <stdio.h>
6
7 #pragma comment(lib, "iphlpapi.lib")
8 #pragma comment(lib, "ws2_32.lib")
9
10 #define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x))
11 #define FREE(x) HeapFree(GetProcessHeap(), 0, (x))
12
13
14
15 int main()
16 {
17
18 19 PMIB_TCPTABLE pTcpTable;
20 DWORD dwSize = 0;
21 DWORD dwRetVal = 0;
22
23 char szLocalAddr[128];
24 char szRemoteAddr[128];
25
26 struct in_addr IpAddr;
27
28 int i;
29
30 pTcpTable = (MIB_TCPTABLE *) MALLOC(sizeof (MIB_TCPTABLE));
31 if (pTcpTable == NULL) {
32 printf("Error allocating memory\n");
33 return 1;
34 }
35
36 dwSize = sizeof (MIB_TCPTABLE);
37 38 39 if ((dwRetVal = GetTcpTable(pTcpTable, &dwSize, TRUE)) ==
40 ERROR_INSUFFICIENT_BUFFER) {
41 FREE(pTcpTable);
42 pTcpTable = (MIB_TCPTABLE *) MALLOC(dwSize);
43 if (pTcpTable == NULL) {
44 printf("Error allocating memory\n");
45 return 1;
46 }
47 }
48 49 50 if ((dwRetVal = GetTcpTable(pTcpTable, &dwSize, TRUE)) == NO_ERROR) {
51 printf("\tNumber of entries: %d\n", (int) pTcpTable->dwNumEntries);
52 for (i = 0; i < (int) pTcpTable->dwNumEntries; i++) {
53 IpAddr.S_un.S_addr = (u_long) pTcpTable->table[i].dwLocalAddr;
54 strcpy_s(szLocalAddr, sizeof (szLocalAddr), inet_ntoa(IpAddr));
55 IpAddr.S_un.S_addr = (u_long) pTcpTable->table[i].dwRemoteAddr;
56 strcpy_s(szRemoteAddr, sizeof (szRemoteAddr), inet_ntoa(IpAddr));
57
58 printf("\n\tTCP[%d] State: %ld - ", i,
59 pTcpTable->table[i].dwState);
60 switch (pTcpTable->table[i].dwState) {
61 case MIB_TCP_STATE_CLOSED:
62 printf("CLOSED\n");
63 break;
64 case MIB_TCP_STATE_LISTEN:
65 printf("LISTEN\n");
66 break;
67 case MIB_TCP_STATE_SYN_SENT:
68 printf("SYN-SENT\n");
69 break;
70 case MIB_TCP_STATE_SYN_RCVD:
71 printf("SYN-RECEIVED\n");
72 break;
73 case MIB_TCP_STATE_ESTAB:
74 printf("ESTABLISHED\n");
75 break;
76 case MIB_TCP_STATE_FIN_WAIT1:
77 printf("FIN-WAIT-1\n");
78 break;
79 case MIB_TCP_STATE_FIN_WAIT2:
80 printf("FIN-WAIT-2 \n");
81 break;
82 case MIB_TCP_STATE_CLOSE_WAIT:
83 printf("CLOSE-WAIT\n");
84 break;
85 case MIB_TCP_STATE_CLOSING:
86 printf("CLOSING\n");
87 break;
88 case MIB_TCP_STATE_LAST_ACK:
89 printf("LAST-ACK\n");
90 break;
91 case MIB_TCP_STATE_TIME_WAIT:
92 printf("TIME-WAIT\n");
93 break;
94 case MIB_TCP_STATE_DELETE_TCB:
95 printf("DELETE-TCB\n");
96 break;
97 default:
98 printf("UNKNOWN dwState value\n");
99 break;
100 }
101 printf("\tTCP[%d] Local Addr: %s\n", i, szLocalAddr);
102 printf("\tTCP[%d] Local Port: %d \n", i,
103 ntohs((u_short)pTcpTable->table[i].dwLocalPort));
104 printf("\tTCP[%d] Remote Addr: %s\n", i, szRemoteAddr);
105 printf("\tTCP[%d] Remote Port: %d\n", i,
106 ntohs((u_short)pTcpTable->table[i].dwRemotePort));
107 }
108 } else {
109 printf("\tGetTcpTable failed with %d\n", dwRetVal);
110 FREE(pTcpTable);
111 return 1;
112 }
113
114 return 0;
115 }
Webonization
For the purpose of this discussion, we define webonization as the process of converting an application's outward facing interfaces to web enabled interfaces. To avoid the complexities of client/server development, we used the Snorkel SDK to webonize the Microsoft example.
In this project, our web-facing interface is HTTP based, and we use an HTML table to display IPv4-TCP connection tables.
.
.
.
39 #define MALLOC(x) snorkel_mem_alloc ((x))
40 #define FREE(x) snorkel_mem_free ((x))
.
.
.
61
65 call_status_t
66 MainPage (snorkel_obj_t http,
67 snorkel_obj_t outstream
68 )
69 {
70
71 PMIB_TCPTABLE pTcpTable;
.
.
.
75 char szquery[2048];
76 char szLocalAddr[128];
77 char szRemoteAddr[128];
78 u_short localPort = 0;
79 u_short remotePort = 0;
80 u_short filter = 0;
81 int header = 0;
82
83 struct in_addr IpAddr;
84
85 int i;
86
87
88
89 pTcpTable =
90 (MIB_TCPTABLE *)
91 MALLOC (sizeof (MIB_TCPTABLE));
92 if (pTcpTable == NULL)
93 {
94 return
95 ERROR_STRING ("Error allocating memory");
96 }
97
98 dwSize = sizeof (MIB_TCPTABLE);
99
100
102 if ((dwRetVal =
103 GetTcpTable (pTcpTable, &dwSize,
104 TRUE)) ==
105 ERROR_INSUFFICIENT_BUFFER)
106 {
107 FREE (pTcpTable);
108 pTcpTable =
109 (MIB_TCPTABLE *) MALLOC (dwSize);
110 if (pTcpTable == NULL)
111 {
112 return
113 ERROR_STRING
114 ("Error allocating memory\n");
115 }
116 }
117
118 szquery[0] = 0;
119 snorkel_obj_get (http, snorkel_attrib_header,
120 "QUERY", szquery,
121 (int) sizeof (szquery));
122
123 if (strlen (szquery) > 0)
124 filter = atoi (szquery);
125
126
128 if ((dwRetVal =
129 GetTcpTable (pTcpTable, &dwSize,
130 TRUE)) == NO_ERROR)
131 {
132
133 if (filter)
134 snorkel_printf (outstream,
135 "<html><header><meta http-equiv=\"refresh\""
136 "content=\"%d\"></header><body><h2>"
137 "MONITORING PORT %d</h2><hr>\r\n",
138 g_autoRefreshInSeconds,
139 filter);
140 else
141 snorkel_printf (outstream,
142 "<html><header><meta http-equiv=\"refresh\""
143 "content=\"%d\"></header><body><h2>"
144 "MONITORING ALL PORTS</h2><hr>\r\n",
145 g_autoRefreshInSeconds);
146
147
148 for (i = 0;
149 i < (int) pTcpTable->dwNumEntries; i++)
150 {
151 IpAddr.S_un.S_addr =
152 (u_long) pTcpTable->table[i].
153 dwLocalAddr;
154 strcpy_s (szLocalAddr,
155 sizeof (szLocalAddr),
156 inet_ntoa (IpAddr));
157 IpAddr.S_un.S_addr =
158 (u_long) pTcpTable->table[i].
159 dwRemoteAddr;
160 strcpy_s (szRemoteAddr,
161 sizeof (szRemoteAddr),
162 inet_ntoa (IpAddr));
163
164 localPort = ntohs ((u_short) pTcpTable->
165 table[i].
166 dwLocalPort);
167 remotePort =
168 ntohs ((u_short) pTcpTable->table[i].
169 dwRemotePort);
170
171 if (filter &&
172 (filter != localPort
173 && filter != remotePort))
174 continue;
175
176 if (!header)
177 {
178 snorkel_printf (outstream,
179 "<div style=\"overflow:auto; width: 100%%; height: 80%%;"
180 "padding:0px; margin: 0px\">\r\n");
181 snorkel_printf (outstream,
182 "<table width=\"90%%\" cellpadding=\"0\">\r\n");
183 snorkel_printf (outstream,
184 "<tr><th align=\"left\">Protocol</th>"
185 "<th align=\"left\">State</th>"
186 "<th align=\"left\">Local Address</th>"
187 "<th align=\"left\">Local Port</th>"
188 "<th align=\"left\">Remote Address</th>"
189 "<th align=\"left\">Remote Port</th></tr>\r\n");
190 header = 1;
191 }
192
193 snorkel_printf (outstream,
194 "<tr><td align=\"left\">tcp</td><td align=\"left\">%02ld - ",
195 pTcpTable->table[i].
196 dwState);
197
198
199 switch (pTcpTable->table[i].dwState)
200 {
201 case MIB_TCP_STATE_CLOSED:
202 snorkel_printf (outstream,
203 "CLOSED</td>");
204 break;
205 case MIB_TCP_STATE_LISTEN:
206 snorkel_printf (outstream,
207 "LISTEN</td>");
208 break;
.
.
.
249 default:
250 snorkel_printf (outstream,
251 "UNKNOWN dwState value</td>");
252 break;
253 }
254
255
256 snorkel_printf (outstream,
257 "<td align=\"left\">%s</td>",
258 szLocalAddr);
259 snorkel_printf (outstream,
260 "<td align=\"left\">%d</td>",
261 localPort);
262 snorkel_printf (outstream,
263 "<td align=\"left\">%s</td>",
264 szRemoteAddr);
265 snorkel_printf (outstream,
266 "<td align=\"left\">%d</td></tr>\r\n",
267 remotePort);
268
269
270 }
271
272 }
273 else
274 {
275 char szError[80];
276 sprintf_s (szError, sizeof (szError),
277 "GetTcpTable failed with %d",
278 dwRetVal);
279 FREE (pTcpTable);
280 return ERROR_STRING (szError);
281 }
282
283
284
285 pUdpTable =
286 (MIB_UDPTABLE *)
287 MALLOC (sizeof (MIB_UDPTABLE));
288
.
.
.
292 return
293 ERROR_STRING
294 ("Error allocating memory\n");
295 return HTTP_SUCCESS;
296 }
297
298 dwSize = sizeof (MIB_UDPTABLE);
299
300
302 if ((dwRetVal =
303 GetUdpTable (pUdpTable, &dwSize,
304 TRUE)) ==
305 ERROR_INSUFFICIENT_BUFFER)
306 {
307 FREE (pUdpTable);
308 pUdpTable =
309 (MIB_UDPTABLE *) MALLOC (dwSize);
310 if (pUdpTable == NULL)
311 {
312 if (!header)
313 return
314 ERROR_STRING
315 ("Error allocating memory\n");
316 else
317 return HTTP_SUCCESS;
318 }
319 }
320
321
322
324 if ((dwRetVal =
325 GetUdpTable (pUdpTable, &dwSize,
326 TRUE)) == NO_ERROR)
327 {
328
329
330
331 for (i = 0;
332 i < (int) pUdpTable->dwNumEntries; i++)
333 {
334 IpAddr.S_un.S_addr =
335 (u_long) pUdpTable->table[i].
336 dwLocalAddr;
337 strcpy_s (szLocalAddr,
338 sizeof (szLocalAddr),
339 inet_ntoa (IpAddr));
340
341 localPort = ntohs ((u_short) pUdpTable->
342 table[i].
343 dwLocalPort);
344
345 if (filter && (filter != localPort))
346 continue;
347
348 if (!header)
349 {
350 snorkel_printf (outstream,
351 "<div style=\"overflow:auto; width: 100%%; height: 80%%;"
352 "padding:0px; margin: 0px\">\r\n");
353 snorkel_printf (outstream,
354 "<table width=\"90%%\" cellpadding=\"0\">\r\n");
355 snorkel_printf (outstream,
356 "<tr><th align=\"left\">Protocol</th>"
357 "<th align=\"left\">State</th>"
358 "<th align=\"left\">Local Address</th>"
359 "<th align=\"left\">Local Port</th>"
360 "<th align=\"left\">Remote Address</th>"
361 "<th align=\"left\">Remote Port</th></tr>\r\n");
362 header = 1;
363 }
364
365 snorkel_printf (outstream,
366 "<tr><td align=\"left\">udp</td>"
367 "<td align=\"left\">** - ****");
368
369 snorkel_printf (outstream,
370 "<td align=\"left\">%s</td>",
371 szLocalAddr);
372 snorkel_printf (outstream,
373 "<td align=\"left\">%d</td>",
374 localPort);
375 snorkel_printf (outstream,
376 "<td align=\"left\">*****</td>");
377 snorkel_printf (outstream,
378 "<td align=\"left\">*****</td></tr>\r\n");
379
380
381 }
382 }
383 else
384 {
385 FREE (pUdpTable);
386 }
387
388
393 if (!header && filter)
394 {
395 snorkel_printf (outstream,
396 "<h3>There are no services listening on port %d</h3>",
397 filter);
398 snorkel_printf (outstream,
399 "<hr><address>Server powered by Snorkel</address>"
400 "</body></html>\r\n");
401 }
402 else
403 snorkel_printf (outstream,
404 "</table></div><hr><address>Server powered by Snorkel"
405 "</address></body></html>\r\n");
406
407 return HTTP_SUCCESS;
408 }
409
We begin with the first couple of lines where we replace the example's version of MALLOC
and FREE
with versions that use the Snorkel APIs snorkel_mem_alloc
and snorkel_mem_free
, respectively. These functions use thread heap storage instead of process heap storage, the storage type used by HeapAlloc
. The Snorkel API calls provide lock free memory allocation and de-allocation, as well as thread-currency, providing better overall performance. You can find more on locality and NUMA architectures on the web.
We changed main
in the example to a subroutine-callback called MainPage
. The callback receives as its arguments a pointer to an HTTP-header and a connection object (output stream). We do not call MainPage
directly; a connection-handler calls the function. Connection-handlers are threads assigned by the Snorkel runtime to process incoming HTTP requests. Without going into too much detail, Snorkel assigns a separate connection-handler for each HTTP request. MainPage
is part of a pairing known as an overloaded URI in Snorkel lingo. An overloaded URI is a URI associated with a function. In main
, we associated the function MainPage
with the URIs "/index.html" and "/". When a user enters either "http://server_addres/" or "http://server_addres /index.html" into a browser, the associated connection-handler calls MainPage
to fulfill the request.
To provide port filtering functionality, that is, filtering content based on a provided port number, we enlist the HTTP query string. Query strings are strings that appear after a URI proceeded by a '?'. For example, the URI "http://localhost:8082?7188" limits output to connections associated with port 7188, and the URI "http://localhost:8082" lists all active connections within the IPv4-TCP table. Snorkel stores query-strings in the HTTP variable QUERY
. On lines 119-121, we use the function snorkel_obj_get
to retrieve the query value, if it exists.
In the rest of the routine, we replace calls to the printf
function with calls to the Snorkel API snorkel_printf
. Both functions use the same format delimiters; however, unlike the printf
function, which directs output to the display, snorkel_printf
directs output to the HTTP response page.
We rely on the macros ERROR_STRING
, HTP_ERROR
, and HTTP_SUCCESS
to pass back the callback's status to the handler thread. The Snorkel API provides two methods for returning callback errors, the macro ERROR_STRING
or the macro HTTP_ERROR
. The ERROR_STRING
macro allows developers to specify their own error message for display in the client's browser, and the HTTP_ERROR
macro uses a generic message. If there are no errors, we return HTTP_SUCCESS
.
411 void
412 main (int argc, char *argv[])
413 {
414 int port = 8082;
415 int i = 0;
416 char *pszLog = 0;
417 char szExit[20];
418 snorkel_obj_t http = 0, logobj = 0;
419
420 fprintf (stderr,
421 "\nnetstat server -- a remote port monitoring server\n\n");
422
423 for (i = 1; i < argc; i++)
424 {
425 if (argv[i][0] == '-')
426 {
427 switch (argv[i][1])
428 {
429 case 'l':
430 if (i + 1 >= argc)
431 syntax (argv[0]);
432 pszLog = argv[i + 1];
433 i++;
434 break;
435 case 'r':
436 if (i + 1 >= argc)
437 syntax (argv[0]);
438 g_autoRefreshInSeconds =
439 atoi (argv[i + 1]);
440 i++;
441 break;
442 default:
443 syntax (argv[0]);
444 break;
445 }
446 }
447 else
448 {
449 port = atoi (argv[i]);
450 break;
451 }
452 }
453
454
455 if (pszLog)
456 {
457 logobj =
458 snorkel_obj_create (snorkel_obj_log,
459 pszLog);
460 if (!logobj)
461 {
462 perror ("could not create log file\n");
463 exit (1);
464 }
465 snorkel_debug (1);
466 }
467
468 if (snorkel_init () != SNORKEL_SUCCESS)
469 {
470 perror ("could not initialize snorkel\n");
471 exit (1);
472 }
473
474 http =
475 snorkel_obj_create (snorkel_obj_server, 2,
476 NULL);
477 if (!http)
478 {
479 perror
480 ("could not create server object!\n");
481 exit (1);
482 }
483
484 if (snorkel_obj_set (http,
485 snorkel_attrib_listener,
486 port,
487 0 )
488 != SNORKEL_SUCCESS)
489 {
490 fprintf (stderr,
491 "could not create listener\n");
492 snorkel_obj_destroy (http);
493 exit (1);
494 }
495
496
501 if (snorkel_obj_set (http,
502 snorkel_attrib_uri,
503 GET,
504 "/index.html",
505 encodingtype_text,
506 MainPage) !=
507 SNORKEL_SUCCESS)
508 {
509 perror ("could not overload index.html");
510 snorkel_obj_destroy (http);
511 exit (1);
512 }
513
514 if (snorkel_obj_set
515 (http, snorkel_attrib_ipvers, IPVERS_IPV4,
516 SOCK_SET) != SNORKEL_SUCCESS)
517 {
518 fprintf (stderr,
519 "error could not set ip version\n");
520 exit (1);
521 }
522
523
528 fprintf (stderr,
529 "\n\n[HTTP] starting embedded server\n");
530 if (snorkel_obj_start (http) != SNORKEL_SUCCESS)
531 {
532 perror ("could not start server\n");
533 snorkel_obj_destroy (http);
534 exit (1);
535 }
536
537
543 fprintf (stderr, "\n[HTTP] started.\n\n"
544 "--hit enter to terminate--\n");
545 fgets (szExit, sizeof (szExit), stdin);
546
547 fprintf (stderr, "[HTTP] bye\n");
548
549
554 snorkel_obj_destroy (http);
555 exit (0);
556 }
We derive our main
from Snorkel boilerplate code (see Snorkel Developer Guide). On lines 468 - 482, we initialize the Snorkel API and create our server object, specifying a thread pool size of two (the second argument of snorkel_obj_create
).
Next, on lines 484-494, we create a listener, binding it to a user defined port or the default port number 8082. On lines 501-512, we map our MainPage
callback to "index.html", and on line 530, we start the server. Since the embedded server runs on its own thread, we prompt the user for input using fgets
to prevent the program from immediately exiting.
Running the Server
You can run the server from the command line or by double clicking it from within the explorer. If a port number is not specified, port 8082 is used. The default refresh rate for an HTML page is five seconds. You can change the refresh rate by specifying the -r option on the command line.
Syntax:
netstatsrv [port] [-h] [-r refresh_rate_in_seconds]
To view the IPv4-TCP table locally, launch the server and start up a web browser. From within the browser, enter the URI "http://localhost/8082". To filter on a port number, enter the URI followed by a '?' and the port number. For example, "http://localhost/8082?8080" will only display table entries related to port 8080.
Final Comments
The source file for this project, netstatsrv.c, is located in the "c" directory of the attached bundle.
I am providing a limited version of the Snorkel SDK used in this project to CodeProject members free of charge. The provided SDK includes binaries for SunOS, Linux, and Microsoft Windows platforms. It also includes this example, along with other examples and full documentation on how to use the Snorkel SDK. Even though Snorkel supports SSL, I have removed SSL versions of the runtime libraries due to US trade laws. The developers guide is located in the doc folder of the attached bundle, you may want to read the guide prior to playing around with this project. If you have any questions and or additional interests in the Snorkel SDK, feel free to contact me at wcapers64@gmail.com.
Related Article(s)
History
- 6.18.2010
Made a minor fix to the syntax function and updated Snorkel to the latest version 1.0. Snorkel 1.0 includes:
- Performance optimization enhancements
- Support for keep-alive
- Support for zero-copy (sendfile)
- Exposed thread governor overload -- now users can create more handlers than there are cores by overloading the governor
- Minor bug fixes
- Added built in page to display information about embedded server. To access page, append root URL with /about or /snorkel.
- 6.21.2010
I normally don't post updates to my libraries this fast, but this time I could not resist the temptation. Build 1.0.1 received a significant performance boost over the weekend, on the Windows side, thanks to changes in how file system information is cached. The changes significantly improved requests per second and average transfer rate by reducing I/O blocking. When bench marked against other web-servers, using Apache's ab test, the performance differences were significant enough to merit this early update.
- 6.21.2010
Noticed and corrected defect on UNIXes
- 6.25.2010 -- Snorkel 1.0.2 update
- Fixed minor thread-heap allocator defect for allocations pushed outside of thread-heap
- Added thread-heap integrity check and auto-repair features
- Completed testing and enabled additional performance enhancements that were introduced but disabled for 1.0.1
Note to adopters of Snorkel: Snorkel is heavily tested on a daily basis, and bugs are often detected and fixed before they are detected in the field. Until there is an official site for Snorkel updates, I will keep the version bundled with this article and other articles that use the library up to date with the latest version of the API.
- 7.8.2010 -- Snorkel 1.0.4 update
- Corrected file mismatch between runtime library files in the bin directory and the lib directory for wintel
- I added ability to toggle off/on the size field transmitted by
snorkel_printf
for non-HTTP streams. To enable or disable the feature, which is on by default for non-HTTP streams, use the following command:
snorkel_obj_set (snorkel_obj_t non_http_stream,
snorkel_attrib_cbprintf, int state (enabled=1,disabled=0))
- Added email function. Syntax:
snorkel_smtp_message (char *smtp_server, int port,
char *from, char *to, char *format_string, arg1, arg2,...argN)
Note: Function works just like printf
.
- Exposed a few more attributes
- Minor bug fixes
- 7.20.2010
- 1.3.2011 – Snorkel 2.0.0.1 update (still free)
- New improved API
- Faster performance
- Added new APIs Aqua (Service SDK) Sailfish (C/C++ application server)
- Platform support MAC OSX, SunOS, Debian Linux, Windows
- New download site: http://snorkelembedded.webs.com