1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/dns/opt_record_rdata.h"
6
7 #include <algorithm>
8 #include <memory>
9 #include <optional>
10 #include <string_view>
11 #include <utility>
12
13 #include "base/big_endian.h"
14 #include "net/dns/dns_response.h"
15 #include "net/dns/dns_test_util.h"
16 #include "net/test/gtest_util.h"
17 #include "testing/gmock/include/gmock/gmock.h"
18 #include "testing/gtest/include/gtest/gtest.h"
19
20 namespace net {
21 namespace {
22
23 using ::testing::ElementsAreArray;
24 using ::testing::IsNull;
25 using ::testing::NotNull;
26 using ::testing::SizeIs;
27
MakeStringPiece(const uint8_t * data,unsigned size)28 std::string_view MakeStringPiece(const uint8_t* data, unsigned size) {
29 const char* data_cc = reinterpret_cast<const char*>(data);
30 return std::string_view(data_cc, size);
31 }
32
TEST(OptRecordRdataTest,ParseOptRecord)33 TEST(OptRecordRdataTest, ParseOptRecord) {
34 // This is just the rdata portion of an OPT record, rather than a complete
35 // record.
36 const uint8_t rdata[] = {
37 // First OPT
38 0x00, 0x01, // OPT code
39 0x00, 0x02, // OPT data size
40 0xDE, 0xAD, // OPT data
41 // Second OPT
42 0x00, 0xFF, // OPT code
43 0x00, 0x04, // OPT data size
44 0xDE, 0xAD, 0xBE, 0xEF // OPT data
45 };
46
47 std::string_view rdata_strpiece = MakeStringPiece(rdata, sizeof(rdata));
48 std::unique_ptr<OptRecordRdata> rdata_obj =
49 OptRecordRdata::Create(rdata_strpiece);
50
51 ASSERT_THAT(rdata_obj, NotNull());
52 ASSERT_EQ(rdata_obj->OptCount(), 2u);
53
54 // Check contains
55 ASSERT_TRUE(rdata_obj->ContainsOptCode(1));
56 ASSERT_FALSE(rdata_obj->ContainsOptCode(30));
57
58 // Check elements
59
60 // Note: When passing string or StringPiece as argument, make sure to
61 // construct arguments with length. Otherwise, strings containing a '\0'
62 // character will be truncated.
63 // https://crbug.com/1348679
64
65 std::unique_ptr<OptRecordRdata::UnknownOpt> opt0 =
66 OptRecordRdata::UnknownOpt::CreateForTesting(1,
67 std::string("\xde\xad", 2));
68 std::unique_ptr<OptRecordRdata::UnknownOpt> opt1 =
69 OptRecordRdata::UnknownOpt::CreateForTesting(
70 255, std::string("\xde\xad\xbe\xef", 4));
71
72 ASSERT_EQ(*(rdata_obj->GetOpts()[0]), *(opt0.get()));
73 ASSERT_EQ(*(rdata_obj->GetOpts()[1]), *(opt1.get()));
74 }
75
TEST(OptRecordRdataTest,ParseOptRecordWithShorterSizeThanData)76 TEST(OptRecordRdataTest, ParseOptRecordWithShorterSizeThanData) {
77 // This is just the rdata portion of an OPT record, rather than a complete
78 // record.
79 const uint8_t rdata[] = {
80 0x00, 0xFF, // OPT code
81 0x00, 0x02, // OPT data size (incorrect, should be 4)
82 0xDE, 0xAD, 0xBE, 0xEF // OPT data
83 };
84
85 DnsRecordParser parser(rdata, sizeof(rdata), 0, /*num_records=*/0);
86 std::string_view rdata_strpiece = MakeStringPiece(rdata, sizeof(rdata));
87
88 std::unique_ptr<OptRecordRdata> rdata_obj =
89 OptRecordRdata::Create(rdata_strpiece);
90 ASSERT_THAT(rdata_obj, IsNull());
91 }
92
TEST(OptRecordRdataTest,ParseOptRecordWithLongerSizeThanData)93 TEST(OptRecordRdataTest, ParseOptRecordWithLongerSizeThanData) {
94 // This is just the rdata portion of an OPT record, rather than a complete
95 // record.
96 const uint8_t rdata[] = {
97 0x00, 0xFF, // OPT code
98 0x00, 0x04, // OPT data size (incorrect, should be 4)
99 0xDE, 0xAD // OPT data
100 };
101
102 DnsRecordParser parser(rdata, sizeof(rdata), 0, /*num_records=*/0);
103 std::string_view rdata_strpiece = MakeStringPiece(rdata, sizeof(rdata));
104
105 std::unique_ptr<OptRecordRdata> rdata_obj =
106 OptRecordRdata::Create(rdata_strpiece);
107 ASSERT_THAT(rdata_obj, IsNull());
108 }
109
TEST(OptRecordRdataTest,CreateEdeOpt)110 TEST(OptRecordRdataTest, CreateEdeOpt) {
111 OptRecordRdata::EdeOpt opt0(22, std::string("Don Quixote"));
112
113 ASSERT_EQ(opt0.data(), std::string("\x00\x16"
114 "Don Quixote",
115 13));
116 ASSERT_EQ(opt0.info_code(), 22u);
117 ASSERT_EQ(opt0.extra_text(), std::string("Don Quixote"));
118
119 std::unique_ptr<OptRecordRdata::EdeOpt> opt1 =
120 OptRecordRdata::EdeOpt::Create(std::string("\x00\x08"
121 "Manhattan",
122 11));
123
124 ASSERT_EQ(opt1->data(), std::string("\x00\x08"
125 "Manhattan",
126 11));
127 ASSERT_EQ(opt1->info_code(), 8u);
128 ASSERT_EQ(opt1->extra_text(), std::string("Manhattan"));
129 }
130
TEST(OptRecordRdataTest,TestEdeInfoCode)131 TEST(OptRecordRdataTest, TestEdeInfoCode) {
132 std::unique_ptr<OptRecordRdata::EdeOpt> edeOpt0 =
133 std::make_unique<OptRecordRdata::EdeOpt>(0, "bullettrain");
134 std::unique_ptr<OptRecordRdata::EdeOpt> edeOpt1 =
135 std::make_unique<OptRecordRdata::EdeOpt>(27, "ferrari");
136 std::unique_ptr<OptRecordRdata::EdeOpt> edeOpt2 =
137 std::make_unique<OptRecordRdata::EdeOpt>(28, "sukrit ganesh");
138 ASSERT_EQ(edeOpt0->GetEnumFromInfoCode(),
139 OptRecordRdata::EdeOpt::EdeInfoCode::kOtherError);
140 ASSERT_EQ(
141 edeOpt1->GetEnumFromInfoCode(),
142 OptRecordRdata::EdeOpt::EdeInfoCode::kUnsupportedNsec3IterationsValue);
143 ASSERT_EQ(edeOpt2->GetEnumFromInfoCode(),
144 OptRecordRdata::EdeOpt::EdeInfoCode::kUnrecognizedErrorCode);
145 ASSERT_EQ(OptRecordRdata::EdeOpt::GetEnumFromInfoCode(15),
146 OptRecordRdata::EdeOpt::kBlocked);
147 }
148
149 // Test that an Opt EDE record is parsed correctly
TEST(OptRecordRdataTest,ParseEdeOptRecords)150 TEST(OptRecordRdataTest, ParseEdeOptRecords) {
151 const uint8_t rdata[] = {
152 // First OPT (non-EDE record)
153 0x00, 0x06, // OPT code (6)
154 0x00, 0x04, // OPT data size (4)
155 0xB0, 0xBA, 0xFE, 0x77, // OPT data (Boba Fett)
156
157 // Second OPT (EDE record)
158 0x00, 0x0F, // OPT code (15 for EDE)
159 0x00, 0x05, // OPT data size (info code + extra text)
160 0x00, 0x0D, // EDE info code (13 for Cached Error)
161 'M', 'T', 'A', // UTF-8 EDE extra text ("MTA")
162
163 // Third OPT (EDE record)
164 0x00, 0x0F, // OPT code (15 for EDE)
165 0x00, 0x06, // OPT data size (info code + extra text)
166 0x00, 0x10, // EDE info code (16 for Censored)
167 'M', 'B', 'T', 'A' // UTF-8 EDE extra text ("MBTA")
168 };
169
170 std::string_view rdata_strpiece = MakeStringPiece(rdata, sizeof(rdata));
171 std::unique_ptr<OptRecordRdata> rdata_obj =
172 OptRecordRdata::Create(rdata_strpiece);
173
174 // Test Size of Query
175 ASSERT_THAT(rdata_obj, NotNull());
176 ASSERT_EQ(rdata_obj->OptCount(), 3u);
177
178 // Test Unknown Opt
179 std::unique_ptr<OptRecordRdata::UnknownOpt> opt0 =
180 OptRecordRdata::UnknownOpt::CreateForTesting(
181 6, std::string("\xb0\xba\xfe\x77", 4));
182
183 ASSERT_THAT(rdata_obj->GetOpts(), SizeIs(3));
184 ASSERT_EQ(*rdata_obj->GetOpts()[0], *opt0.get());
185
186 // Test EDE
187 OptRecordRdata::EdeOpt edeOpt0(13, std::string("MTA", 3));
188 OptRecordRdata::EdeOpt edeOpt1(16, std::string("MBTA", 4));
189
190 ASSERT_THAT(rdata_obj->GetEdeOpts(), SizeIs(2));
191 ASSERT_EQ(*rdata_obj->GetEdeOpts()[0], edeOpt0);
192 ASSERT_EQ(*rdata_obj->GetEdeOpts()[1], edeOpt1);
193
194 // Check that member variables are alright
195 ASSERT_EQ(rdata_obj->GetEdeOpts()[0]->data(), edeOpt0.data());
196 ASSERT_EQ(rdata_obj->GetEdeOpts()[1]->data(), edeOpt1.data());
197
198 ASSERT_EQ(rdata_obj->GetEdeOpts()[0]->extra_text(), std::string("MTA", 3));
199 ASSERT_EQ(rdata_obj->GetEdeOpts()[1]->extra_text(), std::string("MBTA", 4));
200
201 ASSERT_EQ(rdata_obj->GetEdeOpts()[0]->info_code(), edeOpt0.info_code());
202 ASSERT_EQ(rdata_obj->GetEdeOpts()[1]->info_code(), edeOpt1.info_code());
203 }
204
205 // Test the Opt equality operator (and its subclasses as well)
TEST(OptRecordRdataTest,OptEquality)206 TEST(OptRecordRdataTest, OptEquality) {
207 // `rdata_obj0` second opt has extra text "BIOS"
208 // `rdata_obj1` second opt has extra text "BIOO"
209 // Note: rdata_obj0 and rdata_obj1 have 2 common Opts and 1 different one.
210 OptRecordRdata rdata_obj0;
211 rdata_obj0.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
212 6, std::string("\xb0\xba\xfe\x77", 4)));
213 rdata_obj0.AddOpt(
214 std::make_unique<OptRecordRdata::EdeOpt>(13, std::string("USA", 3)));
215 rdata_obj0.AddOpt(
216 std::make_unique<OptRecordRdata::EdeOpt>(16, std::string("BIOS", 4)));
217 ASSERT_EQ(rdata_obj0.OptCount(), 3u);
218
219 OptRecordRdata rdata_obj1;
220 rdata_obj1.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
221 6, std::string("\xb0\xba\xfe\x77", 4)));
222 rdata_obj1.AddOpt(
223 std::make_unique<OptRecordRdata::EdeOpt>(13, std::string("USA", 3)));
224 rdata_obj1.AddOpt(
225 std::make_unique<OptRecordRdata::EdeOpt>(16, std::string("BIOO", 4)));
226 ASSERT_EQ(rdata_obj1.OptCount(), 3u);
227
228 auto opts0 = rdata_obj0.GetOpts();
229 auto opts1 = rdata_obj1.GetOpts();
230 auto edeOpts0 = rdata_obj0.GetEdeOpts();
231 auto edeOpts1 = rdata_obj1.GetEdeOpts();
232 ASSERT_THAT(opts0, SizeIs(3));
233 ASSERT_THAT(opts1, SizeIs(3));
234 ASSERT_THAT(edeOpts0, SizeIs(2));
235 ASSERT_THAT(edeOpts1, SizeIs(2));
236
237 // Opt equality
238 ASSERT_EQ(*opts0[0], *opts1[0]);
239 ASSERT_EQ(*opts0[1], *opts1[1]);
240 ASSERT_NE(*opts0[0], *opts1[1]);
241
242 // EdeOpt equality
243 ASSERT_EQ(*edeOpts0[0], *edeOpts1[0]);
244 ASSERT_NE(*edeOpts0[1], *edeOpts1[1]);
245
246 // EdeOpt equality with Opt
247 ASSERT_EQ(*edeOpts0[0], *opts1[1]);
248 ASSERT_NE(*edeOpts0[1], *opts1[2]);
249
250 // Opt equality with EdeOpt
251 // Should work if raw data matches
252 ASSERT_EQ(*opts1[1], *edeOpts0[0]);
253 ASSERT_NE(*opts1[2], *edeOpts0[1]);
254 }
255
256 // Check that rdata is null if the data section of an EDE record is too small
257 // (<2 bytes)
TEST(OptRecordRdataTest,EdeRecordTooSmall)258 TEST(OptRecordRdataTest, EdeRecordTooSmall) {
259 const uint8_t rdata[] = {
260 0x00, 0x0F, // OPT code (15 for EDE)
261 0x00, 0x01, // OPT data size (info code + extra text)
262 0x00 // Fragment of Info Code
263 };
264
265 std::string_view rdata_strpiece = MakeStringPiece(rdata, sizeof(rdata));
266 std::unique_ptr<OptRecordRdata> rdata_obj =
267 OptRecordRdata::Create(rdata_strpiece);
268 ASSERT_THAT(rdata_obj, IsNull());
269 }
270
271 // Check that an EDE record with no extra text is parsed correctly.
TEST(OptRecordRdataTest,EdeRecordNoExtraText)272 TEST(OptRecordRdataTest, EdeRecordNoExtraText) {
273 const uint8_t rdata[] = {
274 0x00, 0x0F, // OPT code (15 for EDE)
275 0x00, 0x02, // OPT data size (info code + extra text)
276 0x00, 0x05 // Info Code
277 };
278
279 std::string_view rdata_strpiece = MakeStringPiece(rdata, sizeof(rdata));
280 std::unique_ptr<OptRecordRdata> rdata_obj =
281 OptRecordRdata::Create(rdata_strpiece);
282 ASSERT_THAT(rdata_obj, NotNull());
283 ASSERT_THAT(rdata_obj->GetEdeOpts(), SizeIs(1));
284 ASSERT_EQ(rdata_obj->GetEdeOpts()[0]->data(), std::string("\x00\x05", 2));
285 ASSERT_EQ(rdata_obj->GetEdeOpts()[0]->info_code(), 5u);
286 ASSERT_EQ(rdata_obj->GetEdeOpts()[0]->extra_text(), "");
287 }
288
289 // Check that an EDE record with non-UTF-8 fails to parse.
TEST(OptRecordRdataTest,EdeRecordExtraTextNonUTF8)290 TEST(OptRecordRdataTest, EdeRecordExtraTextNonUTF8) {
291 const uint8_t rdata[] = {
292 0x00, 0x0F, // OPT code (15 for EDE)
293 0x00, 0x06, // OPT data size (info code + extra text)
294 0x00, 0x05, // Info Code
295 0xB1, 0x05, 0xF0, 0x0D // Extra Text (non-UTF-8)
296 };
297
298 ASSERT_FALSE(base::IsStringUTF8(std::string("\xb1\x05\xf0\x0d", 4)));
299
300 std::string_view rdata_strpiece = MakeStringPiece(rdata, sizeof(rdata));
301 std::unique_ptr<OptRecordRdata> rdata_obj =
302 OptRecordRdata::Create(rdata_strpiece);
303 ASSERT_THAT(rdata_obj, IsNull());
304 }
305
306 // Check that an EDE record with an unknown info code is parsed correctly.
TEST(OptRecordRdataTest,EdeRecordUnknownInfoCode)307 TEST(OptRecordRdataTest, EdeRecordUnknownInfoCode) {
308 const uint8_t rdata[] = {
309 0x00, 0x0F, // OPT code (15 for EDE)
310 0x00, 0x08, // OPT data size (info code + extra text)
311 0x00, 0x44, // Info Code (68 doesn't exist)
312 'B', 'O', 'S', 'T', 'O', 'N' // Extra Text ("BOSTON")
313 };
314
315 std::string_view rdata_strpiece = MakeStringPiece(rdata, sizeof(rdata));
316 std::unique_ptr<OptRecordRdata> rdata_obj =
317 OptRecordRdata::Create(rdata_strpiece);
318 ASSERT_THAT(rdata_obj, NotNull());
319 ASSERT_THAT(rdata_obj->GetEdeOpts(), SizeIs(1));
320 auto* opt = rdata_obj->GetEdeOpts()[0];
321 ASSERT_EQ(opt->data(), std::string("\x00\x44"
322 "BOSTON",
323 8));
324 ASSERT_EQ(opt->info_code(), 68u);
325 ASSERT_EQ(opt->extra_text(), std::string("BOSTON", 6));
326 ASSERT_EQ(opt->GetEnumFromInfoCode(),
327 OptRecordRdata::EdeOpt::EdeInfoCode::kUnrecognizedErrorCode);
328 }
329
TEST(OptRecordRdataTest,CreatePaddingOpt)330 TEST(OptRecordRdataTest, CreatePaddingOpt) {
331 std::unique_ptr<OptRecordRdata::PaddingOpt> opt0 =
332 std::make_unique<OptRecordRdata::PaddingOpt>(12);
333
334 ASSERT_EQ(opt0->data(), std::string(12, '\0'));
335 ASSERT_THAT(opt0->data(), SizeIs(12u));
336
337 std::unique_ptr<OptRecordRdata::PaddingOpt> opt1 =
338 std::make_unique<OptRecordRdata::PaddingOpt>("MASSACHUSETTS");
339
340 ASSERT_EQ(opt1->data(), std::string("MASSACHUSETTS"));
341 ASSERT_THAT(opt1->data(), SizeIs(13u));
342 }
343
TEST(OptRecordRdataTest,ParsePaddingOpt)344 TEST(OptRecordRdataTest, ParsePaddingOpt) {
345 const uint8_t rdata[] = {
346 // First OPT
347 0x00, 0x0C, // OPT code
348 0x00, 0x07, // OPT data size
349 0xB0, 0x03, // OPT data padding (Book of Boba Fett)
350 0x0F, 0xB0, 0xBA, 0xFE, 0x77,
351 };
352
353 std::string_view rdata_strpiece = MakeStringPiece(rdata, sizeof(rdata));
354 std::unique_ptr<OptRecordRdata> rdata_obj =
355 OptRecordRdata::Create(rdata_strpiece);
356
357 ASSERT_THAT(rdata_obj, NotNull());
358 ASSERT_EQ(rdata_obj->OptCount(), 1u);
359 ASSERT_THAT(rdata_obj->GetOpts(), SizeIs(1));
360 ASSERT_THAT(rdata_obj->GetPaddingOpts(), SizeIs(1));
361
362 // Check elements
363 OptRecordRdata::PaddingOpt opt0(
364 std::string("\xb0\x03\x0f\xb0\xba\xfe\x77", 7));
365
366 ASSERT_EQ(*(rdata_obj->GetOpts()[0]), opt0);
367 ASSERT_EQ(*(rdata_obj->GetPaddingOpts()[0]), opt0);
368 ASSERT_THAT(opt0.data(), SizeIs(7u));
369 }
370
TEST(OptRecordRdataTest,AddOptToOptRecord)371 TEST(OptRecordRdataTest, AddOptToOptRecord) {
372 // This is just the rdata portion of an OPT record, rather than a complete
373 // record.
374 const uint8_t expected_rdata[] = {
375 0x00, 0xFF, // OPT code
376 0x00, 0x04, // OPT data size
377 0xDE, 0xAD, 0xBE, 0xEF // OPT data
378 };
379
380 OptRecordRdata rdata;
381 rdata.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
382 255, std::string("\xde\xad\xbe\xef", 4)));
383 EXPECT_THAT(rdata.buf(), ElementsAreArray(expected_rdata));
384 }
385
386 // Test the OptRecordRdata equality operator.
387 // Equality must be order sensitive. If Opts are same but inserted in different
388 // order, test will fail epically.
TEST(OptRecordRdataTest,EqualityIsOptOrderSensitive)389 TEST(OptRecordRdataTest, EqualityIsOptOrderSensitive) {
390 // Control rdata
391 OptRecordRdata rdata_obj0;
392 rdata_obj0.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
393 1, std::string("\xb0\xba\xfe\x77", 4)));
394 rdata_obj0.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
395 2, std::string("\xb1\x05\xf0\x0d", 4)));
396 ASSERT_EQ(rdata_obj0.OptCount(), 2u);
397
398 // Same as `rdata_obj0`
399 OptRecordRdata rdata_obj1;
400 rdata_obj1.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
401 1, std::string("\xb0\xba\xfe\x77", 4)));
402 rdata_obj1.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
403 2, std::string("\xb1\x05\xf0\x0d", 4)));
404 ASSERT_EQ(rdata_obj1.OptCount(), 2u);
405
406 ASSERT_EQ(rdata_obj0, rdata_obj1);
407
408 // Same contents as `rdata_obj0` & `rdata_obj1`, but different order
409 OptRecordRdata rdata_obj2;
410 rdata_obj2.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
411 2, std::string("\xb1\x05\xf0\x0d", 4)));
412 rdata_obj2.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
413 1, std::string("\xb0\xba\xfe\x77", 4)));
414 ASSERT_EQ(rdata_obj2.OptCount(), 2u);
415
416 // Order matters! obj0 and obj2 contain same Opts but in different order.
417 ASSERT_FALSE(rdata_obj0.IsEqual(&rdata_obj2));
418
419 // Contains only `rdata_obj0` first opt
420 // 2nd opt is added later
421 OptRecordRdata rdata_obj3;
422 rdata_obj3.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
423 1, std::string("\xb0\xba\xfe\x77", 4)));
424 ASSERT_EQ(rdata_obj3.OptCount(), 1u);
425
426 ASSERT_FALSE(rdata_obj0.IsEqual(&rdata_obj3));
427
428 rdata_obj3.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
429 2, std::string("\xb1\x05\xf0\x0d", 4)));
430
431 ASSERT_TRUE(rdata_obj0.IsEqual(&rdata_obj3));
432
433 // Test == operator
434 ASSERT_TRUE(rdata_obj0 == rdata_obj1);
435 ASSERT_EQ(rdata_obj0, rdata_obj1);
436 ASSERT_NE(rdata_obj0, rdata_obj2);
437 }
438
439 // Test that GetOpts() follows specified order.
440 // Sort by key, then by insertion order.
TEST(OptRecordRdataTest,TestGetOptsOrder)441 TEST(OptRecordRdataTest, TestGetOptsOrder) {
442 OptRecordRdata rdata_obj0;
443 rdata_obj0.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
444 10, std::string("\x33\x33", 2)));
445 rdata_obj0.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
446 5, std::string("\x11\x11", 2)));
447 rdata_obj0.AddOpt(OptRecordRdata::UnknownOpt::CreateForTesting(
448 5, std::string("\x22\x22", 2)));
449 ASSERT_EQ(rdata_obj0.OptCount(), 3u);
450
451 auto opts = rdata_obj0.GetOpts();
452 ASSERT_EQ(opts[0]->data(),
453 std::string("\x11\x11", 2)); // opt code 5 (inserted first)
454 ASSERT_EQ(opts[1]->data(),
455 std::string("\x22\x22", 2)); // opt code 5 (inserted second)
456 ASSERT_EQ(opts[2]->data(), std::string("\x33\x33", 2)); // opt code 10
457 }
458
459 } // namespace
460 } // namespace net
461