From 31f02ae7a5105fb6703c34f626ce3798c3cb83c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 26 Jun 2018 16:03:20 +0200 Subject: [PATCH 1/2] Support parsing OPT records (EDNS0) --- src/Model/Message.php | 13 ++++++ src/Model/Record.php | 15 ++++++ src/Protocol/BinaryDumper.php | 6 +++ src/Protocol/Parser.php | 7 +++ tests/Protocol/BinaryDumperTest.php | 71 +++++++++++++++++++++++++++-- tests/Protocol/ParserTest.php | 53 +++++++++++++++++++++ 6 files changed, 162 insertions(+), 3 deletions(-) diff --git a/src/Model/Message.php b/src/Model/Message.php index df42385b..9ff2a30b 100644 --- a/src/Model/Message.php +++ b/src/Model/Message.php @@ -21,6 +21,19 @@ final class Message const TYPE_AAAA = 28; const TYPE_SRV = 33; const TYPE_SSHFP = 44; + + /** + * pseudo-type for EDNS0 + * + * These are included in the additional section and usually not in answer section. + * Defined in [RFC 6891](https://tools.ietf.org/html/rfc6891) (or older + * [RFC 2671](https://tools.ietf.org/html/rfc2671)). + * + * The OPT record uses the "class" field to store the maximum size. + * + * The OPT record uses the "ttl" field to store additional flags. + */ + const TYPE_OPT = 41; const TYPE_ANY = 255; const TYPE_CAA = 257; diff --git a/src/Model/Record.php b/src/Model/Record.php index 4c357ed2..c941ff13 100644 --- a/src/Model/Record.php +++ b/src/Model/Record.php @@ -24,13 +24,23 @@ final class Record public $type; /** + * Defines the network class, usually `Message::CLASS_IN`. + * + * For `OPT` records (EDNS0), this defines the maximum message size instead. + * * @var int see Message::CLASS_IN constant (UINT16) + * @see Message::CLASS_IN */ public $class; /** + * Defines the maximum time-to-live (TTL) in seconds + * + * For `OPT` records (EDNS0), this defines additional flags instead. + * * @var int maximum TTL in seconds (UINT32, most significant bit always unset) * @link https://tools.ietf.org/html/rfc2181#section-8 + * @link https://tools.ietf.org/html/rfc6891#section-6.1.3 for `OPT` records (EDNS0) */ public $ttl; @@ -102,6 +112,11 @@ final class Record * Includes flag (UNIT8), tag string and value string, for example: * `{"flag":128,"tag":"issue","value":"letsencrypt.org"}` * + * - OPT: + * Special pseudo-type for EDNS0. Includes an array of additional opt codes + * with a binary string value in the form `[10=>"\x00",15=>"abc"]`. See + * also [RFC 6891](https://tools.ietf.org/html/rfc6891) for more details. + * * - Any other unknown type: * An opaque binary string containing the RDATA as transported in the DNS * record. For forwards compatibility, you should not rely on this format diff --git a/src/Protocol/BinaryDumper.php b/src/Protocol/BinaryDumper.php index 74d0b72b..73ff400b 100644 --- a/src/Protocol/BinaryDumper.php +++ b/src/Protocol/BinaryDumper.php @@ -139,6 +139,12 @@ private function recordsToBinary(array $records) $record->data['fingerprint'] ); break; + case Message::TYPE_OPT: + $binary = ''; + foreach ($record->data as $opt => $value) { + $binary .= \pack('n*', $opt, \strlen($value)) . $value; + } + break; default: // RDATA is already stored as binary value for unknown record types $binary = $record->data; diff --git a/src/Protocol/Parser.php b/src/Protocol/Parser.php index e94504d4..b2b43020 100644 --- a/src/Protocol/Parser.php +++ b/src/Protocol/Parser.php @@ -230,6 +230,13 @@ private function parseRecord(Message $message) 'minimum' => $minimum ); } + } elseif (Message::TYPE_OPT === $type) { + $rdata = array(); + while (isset($message->data[$consumed + 4 - 1])) { + list($code, $length) = array_values(unpack('n*', substr($message->data, $consumed, 4))); + $rdata[$code] = (string) substr($message->data, $consumed + 4, $length); + $consumed += 4 + $length; + } } elseif (Message::TYPE_CAA === $type) { if ($rdLength > 3) { list($flag, $tagLength) = array_values(unpack('C*', substr($message->data, $consumed, 2))); diff --git a/tests/Protocol/BinaryDumperTest.php b/tests/Protocol/BinaryDumperTest.php index fc360264..8b014255 100644 --- a/tests/Protocol/BinaryDumperTest.php +++ b/tests/Protocol/BinaryDumperTest.php @@ -36,14 +36,76 @@ public function testToBinaryRequestMessage() $this->assertSame($expected, $data); } - public function testToBinaryRequestMessageWithCustomOptForEdns0() + public function testToBinaryRequestMessageWithUnknownAuthorityTypeEncodesValueAsBinary() + { + $data = ""; + $data .= "72 62 01 00 00 01 00 00 00 01 00 00"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "00"; // additional: (empty hostname) + $data .= "d4 31 03 e8 00 00 00 00 00 02 01 02 ";// additional: type OPT, class 1000, TTL 0, binary rdata + + $expected = $this->formatHexDump($data); + + $request = new Message(); + $request->id = 0x7262; + $request->rd = true; + + $request->questions[] = new Query( + 'igor.io', + Message::TYPE_A, + Message::CLASS_IN + ); + + $request->authority[] = new Record('', 54321, 1000, 0, "\x01\x02"); + + $dumper = new BinaryDumper(); + $data = $dumper->toBinary($request); + $data = $this->convertBinaryToHexDump($data); + + $this->assertSame($expected, $data); + } + + public function testToBinaryRequestMessageWithAdditionalOptForEdns0() + { + $data = ""; + $data .= "72 62 01 00 00 01 00 00 00 00 00 01"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "00"; // additional: (empty hostname) + $data .= "00 29 03 e8 00 00 00 00 00 00 "; // additional: type OPT, class 1000 UDP size, TTL 0, no RDATA + + $expected = $this->formatHexDump($data); + + $request = new Message(); + $request->id = 0x7262; + $request->rd = true; + + $request->questions[] = new Query( + 'igor.io', + Message::TYPE_A, + Message::CLASS_IN + ); + + $request->additional[] = new Record('', Message::TYPE_OPT, 1000, 0, array()); + + $dumper = new BinaryDumper(); + $data = $dumper->toBinary($request); + $data = $this->convertBinaryToHexDump($data); + + $this->assertSame($expected, $data); + } + + public function testToBinaryRequestMessageWithAdditionalOptForEdns0WithCustomOptCodes() { $data = ""; $data .= "72 62 01 00 00 01 00 00 00 00 00 01"; // header $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io $data .= "00 01 00 01"; // question: type A, class IN $data .= "00"; // additional: (empty hostname) - $data .= "00 29 03 e8 00 00 00 00 00 00 "; // additional: type OPT, class UDP size, TTL 0, no RDATA + $data .= "00 29 03 e8 00 00 00 00 00 0d "; // additional: type OPT, class 1000 UDP size, TTL 0, 13 bytes RDATA + $data .= "00 a0 00 03 66 6f 6f"; // OPT code 0xa0 encoded + $data .= "00 01 00 02 00 00 "; // OPT code 0x01 encoded $expected = $this->formatHexDump($data); @@ -57,7 +119,10 @@ public function testToBinaryRequestMessageWithCustomOptForEdns0() Message::CLASS_IN ); - $request->additional[] = new Record('', 41, 1000, 0, ''); + $request->additional[] = new Record('', Message::TYPE_OPT, 1000, 0, array( + 0xa0 => 'foo', + 0x01 => "\x00\00" + )); $dumper = new BinaryDumper(); $data = $dumper->toBinary($request); diff --git a/tests/Protocol/ParserTest.php b/tests/Protocol/ParserTest.php index 6ff66a9e..7ec5ea62 100644 --- a/tests/Protocol/ParserTest.php +++ b/tests/Protocol/ParserTest.php @@ -536,6 +536,44 @@ public function testParseSSHFPResponse() $this->assertSame(array('algorithm' => 1, 'type' => 1, 'fingerprint' => '69ac090c'), $response->answers[0]->data); } + public function testParseOptResponseWithoutOptions() + { + $data = ""; + $data .= "00"; // answer: empty domain + $data .= "00 29 03 e8"; // answer: type OPT, class 1000 UDP size + $data .= "00 00 00 00"; // answer: ttl 0 + $data .= "00 00"; // answer: rdlength 0 + + $response = $this->parseAnswer($data); + + $this->assertCount(1, $response->answers); + $this->assertSame('', $response->answers[0]->name); + $this->assertSame(Message::TYPE_OPT, $response->answers[0]->type); + $this->assertSame(1000, $response->answers[0]->class); + $this->assertSame(0, $response->answers[0]->ttl); + $this->assertSame(array(), $response->answers[0]->data); + } + + public function testParseOptResponseWithCustomOptions() + { + $data = ""; + $data .= "00"; // answer: empty domain + $data .= "00 29 03 e8"; // answer: type OPT, class 1000 UDP size + $data .= "00 00 00 00"; // answer: ttl 0 + $data .= "00 0b"; // answer: rdlength 11 + $data .= "00 a0 00 03 66 6f 6f"; // OPT code 0xa0 encoded + $data .= "00 01 00 00 "; // OPT code 0x01 encoded + + $response = $this->parseAnswer($data); + + $this->assertCount(1, $response->answers); + $this->assertSame('', $response->answers[0]->name); + $this->assertSame(Message::TYPE_OPT, $response->answers[0]->type); + $this->assertSame(1000, $response->answers[0]->class); + $this->assertSame(0, $response->answers[0]->ttl); + $this->assertSame(array(0xa0 => 'foo', 0x01 => ''), $response->answers[0]->data); + } + public function testParseSOAResponse() { $data = ""; @@ -957,6 +995,21 @@ public function testParseInvalidSSHFPResponseWhereRecordIsTooSmall() $this->parseAnswer($data); } + /** + * @expectedException InvalidArgumentException + */ + public function testParseInvalidOPTResponseWhereRecordIsTooSmall() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 29 03 e8"; // answer: type OPT, 1000 bytes max size + $data .= "00 00 00 00"; // answer: ttl 0 + $data .= "00 03"; // answer: rdlength 3 + $data .= "00 00 00"; // answer: type 0, length incomplete + + $this->parseAnswer($data); + } + /** * @expectedException InvalidArgumentException */ From 5109a480e9f07bd5000bed3456a850c12c7fc99d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 22 Mar 2020 20:38:58 +0100 Subject: [PATCH 2/2] Add EDNS0 options OPT_TCP_KEEPALIVE and OPT_PADDING --- src/Model/Message.php | 20 ++++++ src/Model/Record.php | 10 ++- src/Protocol/BinaryDumper.php | 3 + src/Protocol/Parser.php | 11 ++- tests/Protocol/BinaryDumperTest.php | 101 +++++++++++++++++++++++++++- tests/Protocol/ParserTest.php | 53 +++++++++++++++ 6 files changed, 194 insertions(+), 4 deletions(-) diff --git a/src/Model/Message.php b/src/Model/Message.php index 9ff2a30b..473526e3 100644 --- a/src/Model/Message.php +++ b/src/Model/Message.php @@ -50,6 +50,26 @@ final class Message const RCODE_NOT_IMPLEMENTED = 4; const RCODE_REFUSED = 5; + /** + * The edns-tcp-keepalive EDNS0 Option + * + * Option value contains a `?float` with timeout in seconds (in 0.1s steps) + * for DNS response or `null` for DNS query. + * + * @link https://tools.ietf.org/html/rfc7828 + */ + const OPT_TCP_KEEPALIVE = 11; + + /** + * The EDNS(0) Padding Option + * + * Option value contains a `string` with binary data (usually variable + * number of null bytes) + * + * @link https://tools.ietf.org/html/rfc7830 + */ + const OPT_PADDING = 12; + /** * Creates a new request message for the given query * diff --git a/src/Model/Record.php b/src/Model/Record.php index c941ff13..c20403f5 100644 --- a/src/Model/Record.php +++ b/src/Model/Record.php @@ -114,8 +114,14 @@ final class Record * * - OPT: * Special pseudo-type for EDNS0. Includes an array of additional opt codes - * with a binary string value in the form `[10=>"\x00",15=>"abc"]`. See - * also [RFC 6891](https://tools.ietf.org/html/rfc6891) for more details. + * with a value according to the respective OPT code. See `Message::OPT_*` + * for list of supported OPT codes. Any other OPT code not currently + * supported will be an opaque binary string containing the raw data + * as transported in the DNS record. For forwards compatibility, you should + * not rely on this format for unknown types. Future versions may add + * support for new types and this may then parse the payload data + * appropriately - this will not be considered a BC break. See also + * [RFC 6891](https://tools.ietf.org/html/rfc6891) for more details. * * - Any other unknown type: * An opaque binary string containing the RDATA as transported in the DNS diff --git a/src/Protocol/BinaryDumper.php b/src/Protocol/BinaryDumper.php index 73ff400b..c0e4962c 100644 --- a/src/Protocol/BinaryDumper.php +++ b/src/Protocol/BinaryDumper.php @@ -142,6 +142,9 @@ private function recordsToBinary(array $records) case Message::TYPE_OPT: $binary = ''; foreach ($record->data as $opt => $value) { + if ($opt === Message::OPT_TCP_KEEPALIVE && $value !== null) { + $value = \pack('n', round($value * 10)); + } $binary .= \pack('n*', $opt, \strlen($value)) . $value; } break; diff --git a/src/Protocol/Parser.php b/src/Protocol/Parser.php index b2b43020..725831fb 100644 --- a/src/Protocol/Parser.php +++ b/src/Protocol/Parser.php @@ -234,7 +234,16 @@ private function parseRecord(Message $message) $rdata = array(); while (isset($message->data[$consumed + 4 - 1])) { list($code, $length) = array_values(unpack('n*', substr($message->data, $consumed, 4))); - $rdata[$code] = (string) substr($message->data, $consumed + 4, $length); + $value = (string) substr($message->data, $consumed + 4, $length); + if ($code === Message::OPT_TCP_KEEPALIVE && $value === '') { + $value = null; + } elseif ($code === Message::OPT_TCP_KEEPALIVE && $length === 2) { + list($value) = array_values(unpack('n', $value)); + $value = round($value * 0.1, 1); + } elseif ($code === Message::OPT_TCP_KEEPALIVE) { + break; + } + $rdata[$code] = $value; $consumed += 4 + $length; } } elseif (Message::TYPE_CAA === $type) { diff --git a/tests/Protocol/BinaryDumperTest.php b/tests/Protocol/BinaryDumperTest.php index 8b014255..c1ab9065 100644 --- a/tests/Protocol/BinaryDumperTest.php +++ b/tests/Protocol/BinaryDumperTest.php @@ -96,6 +96,105 @@ public function testToBinaryRequestMessageWithAdditionalOptForEdns0() $this->assertSame($expected, $data); } + public function testToBinaryRequestMessageWithAdditionalOptForEdns0WithOptTcpKeepAliveDesired() + { + $data = ""; + $data .= "72 62 01 00 00 01 00 00 00 00 00 01"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "00"; // additional: (empty hostname) + $data .= "00 29 03 e8 00 00 00 00 00 04 "; // additional: type OPT, class 1000 UDP size, TTL 0, 4 bytes RDATA + $data .= "00 0b 00 00"; // OPT_TCP_KEEPALIVE=null encoded + + $expected = $this->formatHexDump($data); + + $request = new Message(); + $request->id = 0x7262; + $request->rd = true; + + $request->questions[] = new Query( + 'igor.io', + Message::TYPE_A, + Message::CLASS_IN + ); + + $request->additional[] = new Record('', Message::TYPE_OPT, 1000, 0, array( + Message::OPT_TCP_KEEPALIVE => null, + )); + + $dumper = new BinaryDumper(); + $data = $dumper->toBinary($request); + $data = $this->convertBinaryToHexDump($data); + + $this->assertSame($expected, $data); + } + + public function testToBinaryRequestMessageWithAdditionalOptForEdns0WithOptTcpKeepAliveGiven() + { + $data = ""; + $data .= "72 62 01 00 00 01 00 00 00 00 00 01"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "00"; // additional: (empty hostname) + $data .= "00 29 03 e8 00 00 00 00 00 06 "; // additional: type OPT, class 1000 UDP size, TTL 0, 6 bytes RDATA + $data .= "00 0b 00 02 00 0c"; // OPT_TCP_KEEPALIVE=1.2 encoded + + $expected = $this->formatHexDump($data); + + $request = new Message(); + $request->id = 0x7262; + $request->rd = true; + + $request->questions[] = new Query( + 'igor.io', + Message::TYPE_A, + Message::CLASS_IN + ); + + $request->additional[] = new Record('', Message::TYPE_OPT, 1000, 0, array( + Message::OPT_TCP_KEEPALIVE => 1.2, + )); + + $dumper = new BinaryDumper(); + $data = $dumper->toBinary($request); + $data = $this->convertBinaryToHexDump($data); + + $this->assertSame($expected, $data); + } + + public function testToBinaryRequestMessageWithAdditionalOptForEdns0WithOptPadding() + { + $data = ""; + $data .= "72 62 01 00 00 01 00 00 00 00 00 01"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "00"; // additional: (empty hostname) + $data .= "00 29 03 e8 00 00 00 00 00 06 "; // additional: type OPT, class 1000 UDP size, TTL 0, 6 bytes RDATA + $data .= "00 0c 00 02 00 00 "; // OPT_PADDING=0x0000 encoded + + $expected = $this->formatHexDump($data); + + $request = new Message(); + $request->id = 0x7262; + $request->rd = true; + + $request->questions[] = new Query( + 'igor.io', + Message::TYPE_A, + Message::CLASS_IN + ); + + $request->additional[] = new Record('', Message::TYPE_OPT, 1000, 0, array( + Message::OPT_PADDING => "\x00\x00" + )); + + $dumper = new BinaryDumper(); + $data = $dumper->toBinary($request); + $data = $this->convertBinaryToHexDump($data); + + $this->assertSame($expected, $data); + } + public function testToBinaryRequestMessageWithAdditionalOptForEdns0WithCustomOptCodes() { $data = ""; @@ -121,7 +220,7 @@ public function testToBinaryRequestMessageWithAdditionalOptForEdns0WithCustomOpt $request->additional[] = new Record('', Message::TYPE_OPT, 1000, 0, array( 0xa0 => 'foo', - 0x01 => "\x00\00" + 0x01 => "\x00\x00" )); $dumper = new BinaryDumper(); diff --git a/tests/Protocol/ParserTest.php b/tests/Protocol/ParserTest.php index 7ec5ea62..6efe0481 100644 --- a/tests/Protocol/ParserTest.php +++ b/tests/Protocol/ParserTest.php @@ -554,6 +554,44 @@ public function testParseOptResponseWithoutOptions() $this->assertSame(array(), $response->answers[0]->data); } + public function testParseOptResponseWithOptTcpKeepaliveDesired() + { + $data = ""; + $data .= "00"; // answer: empty domain + $data .= "00 29 03 e8"; // answer: type OPT, class 1000 UDP size + $data .= "00 00 00 00"; // answer: ttl 0 + $data .= "00 04"; // answer: rdlength 4 + $data .= "00 0b 00 00"; // OPT_TCP_KEEPALIVE=null encoded + + $response = $this->parseAnswer($data); + + $this->assertCount(1, $response->answers); + $this->assertSame('', $response->answers[0]->name); + $this->assertSame(Message::TYPE_OPT, $response->answers[0]->type); + $this->assertSame(1000, $response->answers[0]->class); + $this->assertSame(0, $response->answers[0]->ttl); + $this->assertSame(array(Message::OPT_TCP_KEEPALIVE => null), $response->answers[0]->data); + } + + public function testParseOptResponseWithOptTcpKeepaliveGiven() + { + $data = ""; + $data .= "00"; // answer: empty domain + $data .= "00 29 03 e8"; // answer: type OPT, class 1000 UDP size + $data .= "00 00 00 00"; // answer: ttl 0 + $data .= "00 06"; // answer: rdlength 4 + $data .= "00 0b 00 02 00 0c"; // OPT_TCP_KEEPALIVE=1.2s encoded + + $response = $this->parseAnswer($data); + + $this->assertCount(1, $response->answers); + $this->assertSame('', $response->answers[0]->name); + $this->assertSame(Message::TYPE_OPT, $response->answers[0]->type); + $this->assertSame(1000, $response->answers[0]->class); + $this->assertSame(0, $response->answers[0]->ttl); + $this->assertSame(array(Message::OPT_TCP_KEEPALIVE => 1.2), $response->answers[0]->data); + } + public function testParseOptResponseWithCustomOptions() { $data = ""; @@ -1010,6 +1048,21 @@ public function testParseInvalidOPTResponseWhereRecordIsTooSmall() $this->parseAnswer($data); } + /** + * @expectedException InvalidArgumentException + */ + public function testParseInvalidOPTResponseWhereRecordLengthDoesNotMatchOptType() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 29 03 e8"; // answer: type OPT, 1000 bytes max size + $data .= "00 00 00 00"; // answer: ttl 0 + $data .= "00 07"; // answer: rdlength 7 + $data .= "00 0b 00 03 01 02 03"; // answer: type OPT_TCP_KEEPALIVE, length 3 instead of 2 + + $this->parseAnswer($data); + } + /** * @expectedException InvalidArgumentException */