Skip to content

Commit 4f3ac6b

Browse files
bneradtbneradt
authored andcommitted
traffic_dump: add tls information to dump. (#6727)
This change adds tls information nodes like the following: "tls": { "sni": "<SNI>", "verify_mode": "<SSL_VERIFY_MODE_VALUE>" }, Co-authored-by: bneradt <bneradt@verizonmedia.com> (cherry picked from commit bfafd91)
1 parent 0ee6834 commit 4f3ac6b

File tree

4 files changed

+202
-23
lines changed

4 files changed

+202
-23
lines changed

plugins/experimental/traffic_dump/traffic_dump.cc

Lines changed: 146 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ struct StringHashByLower {
7575
};
7676

7777
/// Fields considered sensitive because they may contain user-private
78-
/// information. These fields are replaced with auto-generated generic content
79-
/// by default. To turn off this behavior, the user should add the
80-
/// --promiscuous-mode flag as a commandline argument.
78+
/// information. These fields are replaced with auto-generated generic content by
79+
/// default. To override this behavior, the user should specify their own fields
80+
/// they consider sensitive with --sensitive-fields.
8181
///
8282
/// While these are specified with case, they are matched case-insensitively.
8383
std::unordered_set<std::string, StringHashByLower, InsensitiveCompare> default_sensitive_fields = {
@@ -568,6 +568,143 @@ session_txn_handler(TSCont contp, TSEvent event, void *edata)
568568
return TS_SUCCESS;
569569
}
570570

571+
/** Create a TLS characteristics node.
572+
*
573+
* This function encapsulates the logic common between the client-side and
574+
* server-side logic for populating the TLS node.
575+
*
576+
* @param[in] ssnp The pointer for this session.
577+
*
578+
* @return The node describing the TLS properties of this session.
579+
*/
580+
std::string
581+
get_tls_description_helper(TSVConn ssn_vc)
582+
{
583+
TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc);
584+
SSL *ssl_obj = (SSL *)ssl_conn;
585+
if (ssl_obj == nullptr) {
586+
return "";
587+
}
588+
std::ostringstream tls_description;
589+
tls_description << R"("tls":{)";
590+
const char *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name);
591+
if (sni_ptr != nullptr) {
592+
std::string_view sni{sni_ptr};
593+
if (!sni.empty()) {
594+
tls_description << R"("sni":")" << sni << R"(")";
595+
}
596+
}
597+
tls_description << R"(,"verify_mode":")" << std::to_string(SSL_get_verify_mode(ssl_obj)) << R"(")";
598+
tls_description << "}";
599+
return tls_description.str();
600+
}
601+
602+
/** Create a server-side TLS characteristics node.
603+
*
604+
* @param[in] ssnp The pointer for this session.
605+
*
606+
* @return The node describing the TLS properties of this session.
607+
*/
608+
std::string
609+
get_server_tls_description(TSHttpSsn ssnp)
610+
{
611+
TSVConn ssn_vc = TSHttpSsnServerVConnGet(ssnp);
612+
return get_tls_description_helper(ssn_vc);
613+
}
614+
615+
/** Create a client-side TLS characteristics node.
616+
*
617+
* @param[in] ssnp The pointer for this session.
618+
*
619+
* @return The node describing the TLS properties of this session.
620+
*/
621+
std::string
622+
get_client_tls_description(TSHttpSsn ssnp)
623+
{
624+
TSVConn ssn_vc = TSHttpSsnClientVConnGet(ssnp);
625+
return get_tls_description_helper(ssn_vc);
626+
}
627+
628+
/// A named boolean for callers who pass the is_client parameter.
629+
constexpr bool IS_CLIENT = true;
630+
631+
/** Create the nodes that describe the session's sub-HTTP protocols.
632+
*
633+
* This function encapsulates the logic common between the client-side and
634+
* server-side logic for describing the session's characteristics.
635+
*
636+
* This will create the string representing the "protocol" and "tls" nodes. The
637+
* "tls" node will only be present if the connection is over SSL/TLS.
638+
*
639+
* @param[in] ssnp The pointer for this session.
640+
*
641+
* @return The description of the protocol stack and certain TLS attributes.
642+
*/
643+
std::string
644+
get_protocol_description_helper(TSHttpSsn ssnp, bool is_client)
645+
{
646+
std::ostringstream protocol_description;
647+
protocol_description << R"("protocol":[)";
648+
649+
const char *protocol[10];
650+
int count = -1;
651+
if (is_client) {
652+
TSAssert(TS_SUCCESS == TSHttpSsnClientProtocolStackGet(ssnp, 10, protocol, &count));
653+
} else {
654+
// See the TODO below in the commented out defintion of get_server_protocol_description.
655+
// TSAssert(TS_SUCCESS == TSHttpSsnServerProtocolStackGet(ssnp, 10, protocol, &count));
656+
}
657+
for (int i = 0; i < count; i++) {
658+
if (i > 0) {
659+
protocol_description << ",";
660+
}
661+
protocol_description << '"' << std::string(protocol[i]) << '"';
662+
}
663+
664+
protocol_description << "]";
665+
std::string tls_description;
666+
if (is_client) {
667+
tls_description = get_client_tls_description(ssnp);
668+
} else {
669+
tls_description = get_server_tls_description(ssnp);
670+
}
671+
if (!tls_description.empty()) {
672+
protocol_description << "," << tls_description;
673+
}
674+
return protocol_description.str();
675+
}
676+
677+
#if 0
678+
// TODO It will be important to add this eventually, but
679+
// TSHttpSsnServerProtocolStackGet is not defined yet. Once it (or some other
680+
// mechanism for getting the server side stack) is implemented, we will call
681+
// this as a part of writing the server-response node.
682+
683+
/** Generate the nodes describing the server session.
684+
*
685+
* @param[in] ssnp The pointer for this session.
686+
*
687+
* @return The description of the protocol stack and certain TLS attributes.
688+
*/
689+
std::string
690+
get_server_protocol_description(TSHttpSsn ssnp)
691+
{
692+
return get_protocol_description_helper(ssnp, !IS_CLIENT);
693+
}
694+
#endif
695+
696+
/** Generate the nodes describing the client session.
697+
*
698+
* @param[in] ssnp The pointer for this session.
699+
*
700+
* @return The description of the protocol stack and certain TLS attributes.
701+
*/
702+
std::string
703+
get_client_protocol_description(TSHttpSsn ssnp)
704+
{
705+
return get_protocol_description_helper(ssnp, IS_CLIENT);
706+
}
707+
571708
// Session handler for global hooks; Assign per-session data structure and log files
572709
static int
573710
global_ssn_handler(TSCont contp, TSEvent event, void *edata)
@@ -621,9 +758,9 @@ global_ssn_handler(TSCont contp, TSEvent event, void *edata)
621758
TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore HTTPS session with non-existent SNI.");
622759
break;
623760
} else {
624-
const std::string sni{sni_ptr};
761+
const std::string_view sni{sni_ptr};
625762
if (sni != sni_filter) {
626-
TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore HTTPS session with non-filtered SNI: %s", sni.c_str());
763+
TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore HTTPS session with non-filtered SNI: %s", sni_ptr);
627764
break;
628765
}
629766
}
@@ -647,19 +784,11 @@ global_ssn_handler(TSCont contp, TSEvent event, void *edata)
647784

648785
TSContDataSet(ssnData->aio_cont, ssnData);
649786

650-
// 1. "protocol":(string)
651-
const char *protocol[10];
652-
int count = 0;
653-
TSAssert(TS_SUCCESS == TSHttpSsnClientProtocolStackGet(ssnp, 10, protocol, &count));
654-
std::string result;
655-
for (int i = 0; i < count; i++) {
656-
if (i > 0) {
657-
result += ",";
658-
}
659-
result += '"' + std::string(protocol[i]) + '"';
660-
}
787+
// "protocol":(string),"tls":(string)
788+
// The "tls" node will only be present if the session is over SSL/TLS.
789+
std::string protocol_description = get_client_protocol_description(ssnp);
661790

662-
std::string beginning = R"({"meta":{"version":"1.0"},"sessions":[{"protocol":[)" + result + "]" + R"(,"connection-time":)" +
791+
std::string beginning = R"({"meta":{"version":"1.0"},"sessions":[{)" + protocol_description + R"(,"connection-time":)" +
663792
std::to_string(start.count()) + R"(,"transactions":[)";
664793

665794
// Use the session count's hex string as the filename.

tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,14 @@
103103
tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port))
104104
tr.Processes.Default.StartBefore(Test.Processes.ts)
105105
tr.Processes.Default.Command = \
106-
('curl http://127.0.0.1:{0} -H"Cookie: donotlogthis" '
106+
('curl --http1.1 http://127.0.0.1:{0} -H"Cookie: donotlogthis" '
107107
'-H"Host: www.example.com" -H"X-Request-1: ultra_sensitive" --verbose'.format(
108108
ts.Variables.port))
109109
tr.Processes.Default.ReturnCode = 0
110110
tr.Processes.Default.Streams.stderr = "gold/200.gold"
111111
tr.StillRunningAfter = server
112112
tr.StillRunningAfter = ts
113+
session_1_protocols = "tcp,ipv4"
113114

114115
# Execute the second transaction.
115116
tr = Test.AddTestRun("Second transaction")
@@ -131,11 +132,12 @@
131132
"--sensitive-fields x-request-1 "
132133
"--sensitive-fields x-request-2 ")
133134
tr.Setup.CopyAs(verify_replay, Test.RunDirectory)
134-
tr.Processes.Default.Command = "python3 {0} {1} {2} {3}".format(
135+
tr.Processes.Default.Command = 'python3 {0} {1} {2} {3} --client-protocols "{4}"'.format(
135136
verify_replay,
136137
os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
137138
replay_file_session_1,
138-
sensitive_fields_arg)
139+
sensitive_fields_arg,
140+
session_1_protocols)
139141
tr.Processes.Default.ReturnCode = 0
140142
tr.StillRunningAfter = server
141143
tr.StillRunningAfter = ts

tests/gold_tests/pluginTest/traffic_dump/traffic_dump_sni_filter.test.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,14 @@
119119
tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port))
120120
tr.Processes.Default.StartBefore(Test.Processes.ts)
121121
tr.Processes.Default.Command = \
122-
('curl --tls-max 1.2 -k -H"Host: bob" --resolve "bob:{0}:127.0.0.1" '
122+
('curl --http2 --tls-max 1.2 -k -H"Host: bob" --resolve "bob:{0}:127.0.0.1" '
123123
'--cert ./signed-foo.pem --key ./signed-foo.key --verbose https://bob:{0}'.format(ts.Variables.ssl_port))
124124
tr.Processes.Default.ReturnCode = 0
125125
tr.Processes.Default.Streams.stderr = "gold/200_sni_bob.gold"
126126
tr.StillRunningAfter = server
127127
tr.StillRunningAfter = ts
128+
session_1_protocols = "h2,tls/1.2,tcp,ipv4"
129+
session_1_tls_features = 'sni:bob'
128130

129131
# Execute the second transaction with an SNI of dave.
130132
tr = Test.AddTestRun("Verify that a session of a different SNI is not dumped.")
@@ -150,10 +152,12 @@
150152
tr = Test.AddTestRun("Verify the json content of the first session")
151153
verify_replay = "verify_replay.py"
152154
tr.Setup.CopyAs(verify_replay, Test.RunDirectory)
153-
tr.Processes.Default.Command = "python3 {0} {1} {2}".format(
155+
tr.Processes.Default.Command = 'python3 {0} {1} {2} --client-protocols "{3}" --client-tls-features "{4}"'.format(
154156
verify_replay,
155157
os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
156-
replay_file_session_1)
158+
replay_file_session_1,
159+
session_1_protocols,
160+
session_1_tls_features)
157161
tr.Processes.Default.ReturnCode = 0
158162
tr.StillRunningAfter = server
159163
tr.StillRunningAfter = ts

tests/gold_tests/pluginTest/traffic_dump/verify_replay.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,40 @@ def verify_sensitive_fields_not_dumped(replay_json, sensitive_fields):
139139
return True
140140

141141

142+
def verify_client_protocols(replay_json, expected_protocol_features):
143+
expected_protocols_list = expected_protocol_features.split(',')
144+
expected_protocols_list.sort()
145+
try:
146+
protocol_node = replay_json['sessions'][0]['protocol']
147+
protocol_list = protocol_node.copy()
148+
protocol_list.sort()
149+
if protocol_list == expected_protocols_list:
150+
return True
151+
else:
152+
print('Unexpected protocol stack. Expected: "{}", found: "{}".'.format(
153+
','.join(expected_protocols_list), ','.join(protocol_list)))
154+
return False
155+
except KeyError:
156+
print("Could not find client protocol stack node in the replay file.")
157+
return False
158+
159+
160+
def verify_client_tls_features(replay_json, expected_tls_features):
161+
try:
162+
session = replay_json['sessions'][0]
163+
for expected_tls_feature in expected_tls_features.split(','):
164+
expected_key, expected_value = expected_tls_feature.split(':')
165+
tls_features = session['tls']
166+
try:
167+
return tls_features[expected_key] == expected_value
168+
except KeyError:
169+
print("Could not find client tls feature in the replay file: {}".format(expected_key))
170+
return False
171+
except KeyError:
172+
print("Could not find client tls node in the replay file.")
173+
return False
174+
175+
142176
def parse_args():
143177
parser = argparse.ArgumentParser()
144178
parser.add_argument("schema_file",
@@ -155,6 +189,10 @@ def parse_args():
155189
parser.add_argument("--sensitive-fields",
156190
action="append",
157191
help="The fields that are considered sensitive and replaced with insensitive values.")
192+
parser.add_argument("--client-protocols",
193+
help="The comma-separated protocol features to expect for the client connection.")
194+
parser.add_argument("--client-tls-features",
195+
help="The TLS values to expect for the client connection.")
158196
return parser.parse_args()
159197

160198

@@ -188,6 +226,12 @@ def main():
188226
if args.sensitive_fields and not verify_sensitive_fields_not_dumped(replay_json, args.sensitive_fields):
189227
return 1
190228

229+
if args.client_protocols and not verify_client_protocols(replay_json, args.client_protocols):
230+
return 1
231+
232+
if args.client_tls_features and not verify_client_tls_features(replay_json, args.client_tls_features):
233+
return 1
234+
191235
return 0
192236

193237

0 commit comments

Comments
 (0)