1 /**
2 * Copyright (C) 2019-present MongoDB, Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the Server Side Public License, version 1,
6 * as published by MongoDB, Inc.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * Server Side Public License for more details.
12 *
13 * You should have received a copy of the Server Side Public License
14 * along with this program. If not, see
15 * <http://www.mongodb.com/licensing/server-side-public-license>.
16 *
17 * As a special exception, the copyright holders give permission to link the
18 * code of portions of this program with the OpenSSL library under certain
19 * conditions as described in each individual source file and distribute
20 * linked combinations including the program with the OpenSSL library. You
21 * must comply with the Server Side Public License in all respects for
22 * all of the code used other than as permitted herein. If you modify file(s)
23 * with this exception, you may extend this exception to your version of the
24 * file(s), but you are not obligated to do so. If you do not wish to do so,
25 * delete this exception statement from your version. If you delete this
26 * exception statement from all source files in the program, then also delete
27 * it in the license file.
28 */
29
30 #include "mongo/platform/basic.h"
31
32 #include "mongo/db/repl/split_horizon.h"
33
34 #include <algorithm>
35 #include <boost/optional.hpp>
36 #include <iterator>
37
38 #include "mongo/stdx/utility.h"
39 #include "mongo/unittest/unittest.h"
40
41 using namespace std::literals::string_literals;
42
43 namespace mongo {
44 namespace repl {
45 namespace {
46 static const std::string defaultHost = "default.dns.name.example.com";
47 static const std::string defaultPort = "4242";
48 static const std::string defaultHostAndPort = defaultHost + ":" + defaultPort;
49
50 static const std::string matchingHost = "matching.dns.name.example.com";
51 static const std::string matchingPort = "4243";
52 static const std::string matchingHostAndPort = matchingHost + ":" + matchingPort;
53
54
55 static const std::string nonmatchingHost = "nonmatching.dns.name.example.com";
56 static const std::string nonmatchingPort = "4244";
57 static const std::string nonmatchingHostAndPort = nonmatchingHost + ":" + nonmatchingPort;
58
59 static const std::string altPort = ":666";
60
61 using MappingType = std::map<std::string, std::string>;
62
populateForwardMap(const MappingType & mapping)63 SplitHorizon::ForwardMapping populateForwardMap(const MappingType& mapping) {
64 SplitHorizon::ForwardMapping forwardMapping;
65 forwardMapping.emplace(SplitHorizon::kDefaultHorizon, HostAndPort(defaultHostAndPort));
66
67 using ForwardMappingValueType = decltype(forwardMapping)::value_type;
68 using ElementType = MappingType::value_type;
69 auto createForwardMapping = [](const ElementType& element) {
70 return ForwardMappingValueType{element.first, HostAndPort(element.second)};
71 };
72 std::transform(begin(mapping),
73 end(mapping),
74 inserter(forwardMapping, end(forwardMapping)),
75 createForwardMapping);
76
77 return forwardMapping;
78 }
79
TEST(SplitHorizonTesting,determineHorizon)80 TEST(SplitHorizonTesting, determineHorizon) {
81 struct Input {
82 SplitHorizon::ForwardMapping forwardMapping; // Will get "__default" added to it.
83 SplitHorizon::Parameters horizonParameters;
84
85 Input(const MappingType& mapping, boost::optional<std::string> sniName)
86 : forwardMapping(populateForwardMap(mapping)), horizonParameters(std::move(sniName)) {}
87 };
88
89 struct {
90 Input input;
91
92 std::string expected;
93 } tests[] = {
94 // No parameters and no horizon views configured.
95 {{{}, boost::none}, "__default"},
96 {{{}, defaultHost}, "__default"},
97
98 // No SNI -> no match
99 {{{{"unusedHorizon", "badmatch:00001"}}, boost::none}, "__default"},
100
101 // Unmatching SNI -> no match
102 {{{{"unusedHorizon", "badmatch:00001"}}, nonmatchingHost}, "__default"},
103
104 // Matching SNI -> match
105 {{{{"targetHorizon", matchingHostAndPort}}, matchingHost}, "targetHorizon"},
106 };
107
108 for (const auto& test : tests) {
109 const auto& expected = test.expected;
110 const auto& input = test.input;
111
112 const std::string witness =
113 SplitHorizon(input.forwardMapping).determineHorizon(input.horizonParameters).toString();
114 ASSERT_EQUALS(witness, expected);
115 }
116
117 const Input failingCtorCases[] = {
118 // Matching SNI, different port, collision -> fails
119 {{{"targetHorizon", matchingHost + altPort}, {"badHorizon", matchingHostAndPort}},
120 matchingHost},
121
122 // Default horizon ambiguous case is a failure
123 {{{"targetHorizon", defaultHost + altPort}, {"badHorizon", nonmatchingHostAndPort}},
124 defaultHost},
125 };
126
127 for (const auto& input : failingCtorCases) {
128 ASSERT_THROWS(SplitHorizon(input.forwardMapping), ExceptionFor<ErrorCodes::BadValue>);
129 }
130 }
131
TEST(SplitHorizonTesting,basicConstruction)132 TEST(SplitHorizonTesting, basicConstruction) {
133 struct Input {
134 SplitHorizon::ForwardMapping forwardMapping; // Will get "__default" added to it.
135
136 Input(const MappingType& mapping) : forwardMapping(populateForwardMap(mapping)) {}
137 };
138
139 const struct {
140 Input input;
141 ErrorCodes::Error expectedErrorCode;
142 std::vector<std::string> expectedErrorMessageFragments;
143 std::vector<std::string> absentErrorMessageFragments;
144 } tests[] = {
145 // Empty case (the `Input` type constructs the expected "__default" member.)
146 {{{}}, ErrorCodes::OK, {}, {}},
147
148 // A single horizon case, with no conflicts.
149 {{{{"extraHorizon", "example.com:42"}}}, ErrorCodes::OK, {}, {}},
150
151 // Two horizons with no conflicts
152 {{{{"extraHorizon", "example.com:42"}, {"extraHorizon2", "extra.example.com:42"}}},
153 ErrorCodes::OK,
154 {},
155 {}},
156
157 // Three horizons with no conflicts
158 {{{{"extraHorizon", "example.com:42"},
159 {"extraHorizon2", "extra.example.com:42"},
160 {"extraHorizon3", "extra3.example.com" + altPort}}},
161 ErrorCodes::OK,
162 {},
163 {}},
164
165 // Two horizons, with the same host and port
166 {{{{"horizon1", "same.example.com:42"}, {"horizon2", "same.example.com:42"}}},
167 ErrorCodes::BadValue,
168 {"Duplicate horizon member found", "same.example.com"},
169 {}},
170
171 // Two horizons, with the same host and different port
172 {{{{"horizon1", "same.example.com:42"}, {"horizon2", "same.example.com:43"}}},
173 ErrorCodes::BadValue,
174 {"Duplicate horizon member found", "same.example.com"},
175 {}},
176
177 // Three horizons, with two of them having the same host and port (checking that
178 // the distinct horizon isn't reported in the error message.
179 {{{{"horizon1", "same.example.com:42"},
180 {"horizon2", "different.example.com:42"},
181 {"horizon3", "same.example.com:42"}}},
182 ErrorCodes::BadValue,
183 {"Duplicate horizon member found", "same.example.com"},
184 {"different.example.com"}},
185 };
186
187 for (const auto& test : tests) {
188 const auto& input = test.input;
189 const auto& expectedErrorCode = test.expectedErrorCode;
190 const auto horizonOpt = [&]() -> boost::optional<SplitHorizon> {
191 try {
192 return SplitHorizon(input.forwardMapping);
193 } catch (const DBException& ex) {
194 ASSERT_NOT_EQUALS(expectedErrorCode, ErrorCodes::OK);
195 ASSERT_EQUALS(ex.toStatus().code(), expectedErrorCode);
196 for (const auto& fragment : test.expectedErrorMessageFragments) {
197 ASSERT_NOT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos);
198 }
199 for (const auto& fragment : test.absentErrorMessageFragments) {
200 ASSERT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos);
201 }
202 return boost::none;
203 }
204 }();
205
206 if (!horizonOpt)
207 continue;
208 ASSERT_EQUALS(expectedErrorCode, ErrorCodes::OK);
209
210 const auto& horizon = *horizonOpt;
211
212 for (const auto& element : input.forwardMapping) {
213 {
214 const auto found = horizon.getForwardMappings().find(element.first);
215 ASSERT_TRUE(found != end(horizon.getForwardMappings()));
216 ASSERT_EQUALS(HostAndPort(element.second).toString(), found->second.toString());
217 }
218
219 {
220 const auto found =
221 horizon.getReverseHostMappings().find(HostAndPort(element.second).host());
222 ASSERT_TRUE(found != end(horizon.getReverseHostMappings()));
223 ASSERT_EQUALS(element.first, found->second);
224 }
225 }
226 ASSERT_EQUALS(input.forwardMapping.size(), horizon.getForwardMappings().size());
227 ASSERT_EQUALS(input.forwardMapping.size(), horizon.getReverseHostMappings().size());
228 }
229 }
230
TEST(SplitHorizonTesting,BSONConstruction)231 TEST(SplitHorizonTesting, BSONConstruction) {
232 // The none-case can be tested outside ot the table, to help keep the table ctors
233 // easier.
234 {
235 const SplitHorizon horizon(HostAndPort(matchingHostAndPort), boost::none);
236
237 {
238 const auto forwardFound = horizon.getForwardMappings().find("__default");
239 ASSERT_TRUE(forwardFound != end(horizon.getForwardMappings()));
240 ASSERT_EQUALS(forwardFound->second, HostAndPort(matchingHostAndPort));
241 ASSERT_EQUALS(horizon.getForwardMappings().size(), std::size_t{1});
242 }
243
244 {
245 const auto reverseFound = horizon.getReverseHostMappings().find(matchingHost);
246 ASSERT_TRUE(reverseFound != end(horizon.getReverseHostMappings()));
247 ASSERT_EQUALS(reverseFound->second, "__default");
248
249 ASSERT_EQUALS(horizon.getReverseHostMappings().size(), std::size_t{1});
250 }
251 }
252
253 const struct {
254 BSONObj bsonContents;
255 std::string host;
256 std::vector<std::pair<std::string, std::string>> expectedMapping; // bidirectional
257 ErrorCodes::Error expectedErrorCode;
258 std::vector<std::string> expectedErrorMessageFragments;
259 std::vector<std::string> absentErrorMessageFragments;
260 } tests[] = {
261 // Empty bson object
262 {BSONObj(),
263 defaultHostAndPort,
264 {},
265 ErrorCodes::BadValue,
266 {"horizons field cannot be empty, if present"},
267 {"example.com"}},
268
269 // One simple horizon case.
270 {BSON("horizon" << matchingHostAndPort),
271 defaultHostAndPort,
272 {{"__default", defaultHostAndPort}, {"horizon", matchingHostAndPort}},
273 ErrorCodes::OK,
274 {},
275 {}},
276
277 // Two simple horizons case
278 {BSON("horizon" << matchingHostAndPort << "horizon2" << nonmatchingHostAndPort),
279 defaultHostAndPort,
280 {{"__default", defaultHostAndPort},
281 {"horizon", matchingHostAndPort},
282 {"horizon2", nonmatchingHostAndPort}},
283 ErrorCodes::OK,
284 {},
285 {}},
286
287 // Three horizons, two having duplicate names
288 {
289 BSON("duplicateHorizon"
290 << "horizon1.example.com:42"
291 << "duplicateHorizon"
292 << "horizon2.example.com:42"
293 << "uniqueHorizon"
294 << "horizon3.example.com:42"),
295 defaultHostAndPort,
296 {},
297 ErrorCodes::BadValue,
298 {"Duplicate horizon name found", "duplicateHorizon"},
299 {"uniqueHorizon", "__default"}},
300
301 // Two horizons with duplicate host and ports.
302 {BSON("horizonWithDuplicateHost1" << matchingHostAndPort << "horizonWithDuplicateHost2"
303 << matchingHostAndPort
304 << "uniqueHorizon"
305 << nonmatchingHost),
306 defaultHostAndPort,
307 {},
308 ErrorCodes::BadValue,
309 {"Duplicate horizon member found", matchingHost},
310 {"uniqueHorizon", nonmatchingHost, defaultHost}},
311 };
312
313 for (const auto& test : tests) {
314 const auto testNumber = &test - tests;
315 const BSONObj bson = BSON("horizons" << test.bsonContents);
316 const auto& expectedErrorCode = test.expectedErrorCode;
317
318 const auto horizonOpt = [&]() -> boost::optional<SplitHorizon> {
319 const auto host = HostAndPort(test.host);
320 const auto& bsonElement = bson.firstElement();
321 try {
322 return SplitHorizon(host, bsonElement);
323 } catch (const DBException& ex) {
324 ASSERT_NOT_EQUALS(expectedErrorCode, ErrorCodes::OK)
325 << "Failing on test case # " << (&test - tests)
326 << " with unexpected failure: " << ex.toStatus().reason();
327 ASSERT_EQUALS(ex.toStatus().code(), expectedErrorCode)
328 << "Failing status code comparison on test case " << testNumber
329 << " reason: " << ex.toStatus().reason();
330 for (const auto& fragment : test.expectedErrorMessageFragments) {
331 ASSERT_NOT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos)
332 << "Wanted to see the text fragment \"" << fragment
333 << "\" in the message: \"" << ex.toStatus().reason() << "\"";
334 }
335 for (const auto& fragment : test.absentErrorMessageFragments) {
336 ASSERT_EQUALS(ex.toStatus().reason().find(fragment), std::string::npos);
337 }
338 return boost::none;
339 }
340 }();
341
342 if (!horizonOpt)
343 continue;
344 ASSERT_EQUALS(expectedErrorCode, ErrorCodes::OK);
345
346 const auto& horizon = *horizonOpt;
347
348 for (const auto& element : test.expectedMapping) {
349 {
350 const auto found = horizon.getForwardMappings().find(element.first);
351 ASSERT_TRUE(found != end(horizon.getForwardMappings()));
352 ASSERT_EQUALS(HostAndPort(element.second).toString(), found->second.toString());
353 }
354
355 {
356 const auto found =
357 horizon.getReverseHostMappings().find(HostAndPort(element.second).host());
358 ASSERT_TRUE(found != end(horizon.getReverseHostMappings()))
359 << "Failed test # " << testNumber
360 << " because we didn't find a reverse mapping for the host " << element.first;
361 ASSERT_EQUALS(element.first, found->second) << "on test " << testNumber;
362 }
363 }
364
365 ASSERT_EQUALS(test.expectedMapping.size(), horizon.getForwardMappings().size());
366 ASSERT_EQUALS(test.expectedMapping.size(), horizon.getReverseHostMappings().size());
367 }
368 }
369
TEST(SplitHorizonTesting,toBSON)370 TEST(SplitHorizonTesting, toBSON) {
371 struct Input {
372 SplitHorizon::ForwardMapping forwardMapping; // Will get "__default" added to it.
373
374 Input(const MappingType& mapping) : forwardMapping(populateForwardMap(mapping)) {}
375 };
376
377 const Input tests[] = {
378 {{}},
379 {{{"horizon1", "horizon1.example.com:42"}}},
380 {{{"horizon1", "horizon1.example.com:42"}, {"horizon2", "horizon2.example.com:42"}}},
381 {{{"horizon1", "horizon1.example.com:42"},
382 {"horizon2", "horizon2.example.com:42"},
383 {"horizon3", "horizon3.example.com:99"}}},
384 };
385 for (const auto& test : tests) {
386 const auto testNumber = &test - tests;
387 const auto& input = test;
388 const auto& expectedKeys = [&] {
389 auto rv = input.forwardMapping;
390 rv.erase(SplitHorizon::kDefaultHorizon);
391 return rv;
392 }();
393
394 const SplitHorizon horizon(input.forwardMapping);
395
396 const BSONObj output = [&] {
397 BSONObjBuilder outputBuilder;
398 horizon.toBSON(outputBuilder);
399 return outputBuilder.obj();
400 }();
401
402 if (expectedKeys.empty()) {
403 ASSERT_TRUE(output["horizons"].eoo());
404 continue;
405 }
406
407 ASSERT_FALSE(output["horizons"].eoo());
408
409 for (const auto& horizons : output) {
410 ASSERT_TRUE(horizons.fieldNameStringData() == "horizons")
411 << "Test #" << testNumber << " Failing finding a horizons element";
412 }
413
414 const auto& horizonsElement = output["horizons"];
415 ASSERT_EQUALS(horizonsElement.type(), Object);
416 const auto& horizons = horizonsElement.Obj();
417
418 std::vector<std::string> visitedHorizons;
419
420 for (const auto& element : horizons) {
421 ASSERT_TRUE(expectedKeys.count(element.fieldNameStringData().toString()))
422 << "Test #" << testNumber << " Failing because we found a horizon with the name "
423 << element.fieldNameStringData() << " which shouldn't exist.";
424
425 ASSERT_TRUE(
426 expectedKeys.find(element.fieldNameStringData().toString())->second.toString() ==
427 element.valueStringData())
428 << "Test #" << testNumber << " failing because horizon "
429 << element.fieldNameStringData() << " had an incorrect HostAndPort";
430 visitedHorizons.push_back(element.fieldNameStringData().toString());
431 }
432
433 ASSERT_EQUALS(visitedHorizons.size(), expectedKeys.size());
434 }
435 }
436
TEST(SplitHorizonTesting,BSONRoundTrip)437 TEST(SplitHorizonTesting, BSONRoundTrip) {
438 struct Input {
439 SplitHorizon::ForwardMapping forwardMapping; // Will get "__default" added to it.
440
441 Input(const MappingType& mapping) : forwardMapping(populateForwardMap(mapping)) {}
442 };
443
444 const Input tests[] = {
445 {{{"horizon1", "horizon1.example.com:42"}}},
446 {{{"horizon1", "horizon1.example.com:42"}, {"horizon2", "horizon2.example.com:42"}}},
447 };
448 for (const auto& input : tests) {
449 const auto testNumber = &input - tests;
450
451 const SplitHorizon horizon(input.forwardMapping);
452
453 const BSONObj bson = [&] {
454 BSONObjBuilder outputBuilder;
455 horizon.toBSON(outputBuilder);
456 return outputBuilder.obj();
457 }();
458
459 const SplitHorizon witness(HostAndPort(defaultHostAndPort), bson["horizons"]);
460
461 ASSERT_TRUE(horizon.getForwardMappings() == witness.getForwardMappings())
462 << "Test #" << testNumber << " Failed on bson round trip with forward map";
463 ASSERT_TRUE(horizon.getReverseHostMappings() == witness.getReverseHostMappings())
464 << "Test #" << testNumber << " Failed on bson round trip with reverse map";
465 }
466 }
467 } // namespace
468 } // namespace repl
469 } // namespace mongo
470