Skip to content

Commit 31f02ae

Browse files
committed
Support parsing OPT records (EDNS0)
1 parent 5a74b81 commit 31f02ae

6 files changed

Lines changed: 162 additions & 3 deletions

File tree

src/Model/Message.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ final class Message
2121
const TYPE_AAAA = 28;
2222
const TYPE_SRV = 33;
2323
const TYPE_SSHFP = 44;
24+
25+
/**
26+
* pseudo-type for EDNS0
27+
*
28+
* These are included in the additional section and usually not in answer section.
29+
* Defined in [RFC 6891](https://tools.ietf.org/html/rfc6891) (or older
30+
* [RFC 2671](https://tools.ietf.org/html/rfc2671)).
31+
*
32+
* The OPT record uses the "class" field to store the maximum size.
33+
*
34+
* The OPT record uses the "ttl" field to store additional flags.
35+
*/
36+
const TYPE_OPT = 41;
2437
const TYPE_ANY = 255;
2538
const TYPE_CAA = 257;
2639

src/Model/Record.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,23 @@ final class Record
2424
public $type;
2525

2626
/**
27+
* Defines the network class, usually `Message::CLASS_IN`.
28+
*
29+
* For `OPT` records (EDNS0), this defines the maximum message size instead.
30+
*
2731
* @var int see Message::CLASS_IN constant (UINT16)
32+
* @see Message::CLASS_IN
2833
*/
2934
public $class;
3035

3136
/**
37+
* Defines the maximum time-to-live (TTL) in seconds
38+
*
39+
* For `OPT` records (EDNS0), this defines additional flags instead.
40+
*
3241
* @var int maximum TTL in seconds (UINT32, most significant bit always unset)
3342
* @link https://tools.ietf.org/html/rfc2181#section-8
43+
* @link https://tools.ietf.org/html/rfc6891#section-6.1.3 for `OPT` records (EDNS0)
3444
*/
3545
public $ttl;
3646

@@ -102,6 +112,11 @@ final class Record
102112
* Includes flag (UNIT8), tag string and value string, for example:
103113
* `{"flag":128,"tag":"issue","value":"letsencrypt.org"}`
104114
*
115+
* - OPT:
116+
* Special pseudo-type for EDNS0. Includes an array of additional opt codes
117+
* with a binary string value in the form `[10=>"\x00",15=>"abc"]`. See
118+
* also [RFC 6891](https://tools.ietf.org/html/rfc6891) for more details.
119+
*
105120
* - Any other unknown type:
106121
* An opaque binary string containing the RDATA as transported in the DNS
107122
* record. For forwards compatibility, you should not rely on this format

src/Protocol/BinaryDumper.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ private function recordsToBinary(array $records)
139139
$record->data['fingerprint']
140140
);
141141
break;
142+
case Message::TYPE_OPT:
143+
$binary = '';
144+
foreach ($record->data as $opt => $value) {
145+
$binary .= \pack('n*', $opt, \strlen($value)) . $value;
146+
}
147+
break;
142148
default:
143149
// RDATA is already stored as binary value for unknown record types
144150
$binary = $record->data;

src/Protocol/Parser.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ private function parseRecord(Message $message)
230230
'minimum' => $minimum
231231
);
232232
}
233+
} elseif (Message::TYPE_OPT === $type) {
234+
$rdata = array();
235+
while (isset($message->data[$consumed + 4 - 1])) {
236+
list($code, $length) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
237+
$rdata[$code] = (string) substr($message->data, $consumed + 4, $length);
238+
$consumed += 4 + $length;
239+
}
233240
} elseif (Message::TYPE_CAA === $type) {
234241
if ($rdLength > 3) {
235242
list($flag, $tagLength) = array_values(unpack('C*', substr($message->data, $consumed, 2)));

tests/Protocol/BinaryDumperTest.php

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,76 @@ public function testToBinaryRequestMessage()
3636
$this->assertSame($expected, $data);
3737
}
3838

39-
public function testToBinaryRequestMessageWithCustomOptForEdns0()
39+
public function testToBinaryRequestMessageWithUnknownAuthorityTypeEncodesValueAsBinary()
40+
{
41+
$data = "";
42+
$data .= "72 62 01 00 00 01 00 00 00 01 00 00"; // header
43+
$data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
44+
$data .= "00 01 00 01"; // question: type A, class IN
45+
$data .= "00"; // additional: (empty hostname)
46+
$data .= "d4 31 03 e8 00 00 00 00 00 02 01 02 ";// additional: type OPT, class 1000, TTL 0, binary rdata
47+
48+
$expected = $this->formatHexDump($data);
49+
50+
$request = new Message();
51+
$request->id = 0x7262;
52+
$request->rd = true;
53+
54+
$request->questions[] = new Query(
55+
'igor.io',
56+
Message::TYPE_A,
57+
Message::CLASS_IN
58+
);
59+
60+
$request->authority[] = new Record('', 54321, 1000, 0, "\x01\x02");
61+
62+
$dumper = new BinaryDumper();
63+
$data = $dumper->toBinary($request);
64+
$data = $this->convertBinaryToHexDump($data);
65+
66+
$this->assertSame($expected, $data);
67+
}
68+
69+
public function testToBinaryRequestMessageWithAdditionalOptForEdns0()
70+
{
71+
$data = "";
72+
$data .= "72 62 01 00 00 01 00 00 00 00 00 01"; // header
73+
$data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
74+
$data .= "00 01 00 01"; // question: type A, class IN
75+
$data .= "00"; // additional: (empty hostname)
76+
$data .= "00 29 03 e8 00 00 00 00 00 00 "; // additional: type OPT, class 1000 UDP size, TTL 0, no RDATA
77+
78+
$expected = $this->formatHexDump($data);
79+
80+
$request = new Message();
81+
$request->id = 0x7262;
82+
$request->rd = true;
83+
84+
$request->questions[] = new Query(
85+
'igor.io',
86+
Message::TYPE_A,
87+
Message::CLASS_IN
88+
);
89+
90+
$request->additional[] = new Record('', Message::TYPE_OPT, 1000, 0, array());
91+
92+
$dumper = new BinaryDumper();
93+
$data = $dumper->toBinary($request);
94+
$data = $this->convertBinaryToHexDump($data);
95+
96+
$this->assertSame($expected, $data);
97+
}
98+
99+
public function testToBinaryRequestMessageWithAdditionalOptForEdns0WithCustomOptCodes()
40100
{
41101
$data = "";
42102
$data .= "72 62 01 00 00 01 00 00 00 00 00 01"; // header
43103
$data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
44104
$data .= "00 01 00 01"; // question: type A, class IN
45105
$data .= "00"; // additional: (empty hostname)
46-
$data .= "00 29 03 e8 00 00 00 00 00 00 "; // additional: type OPT, class UDP size, TTL 0, no RDATA
106+
$data .= "00 29 03 e8 00 00 00 00 00 0d "; // additional: type OPT, class 1000 UDP size, TTL 0, 13 bytes RDATA
107+
$data .= "00 a0 00 03 66 6f 6f"; // OPT code 0xa0 encoded
108+
$data .= "00 01 00 02 00 00 "; // OPT code 0x01 encoded
47109

48110
$expected = $this->formatHexDump($data);
49111

@@ -57,7 +119,10 @@ public function testToBinaryRequestMessageWithCustomOptForEdns0()
57119
Message::CLASS_IN
58120
);
59121

60-
$request->additional[] = new Record('', 41, 1000, 0, '');
122+
$request->additional[] = new Record('', Message::TYPE_OPT, 1000, 0, array(
123+
0xa0 => 'foo',
124+
0x01 => "\x00\00"
125+
));
61126

62127
$dumper = new BinaryDumper();
63128
$data = $dumper->toBinary($request);

tests/Protocol/ParserTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,44 @@ public function testParseSSHFPResponse()
536536
$this->assertSame(array('algorithm' => 1, 'type' => 1, 'fingerprint' => '69ac090c'), $response->answers[0]->data);
537537
}
538538

539+
public function testParseOptResponseWithoutOptions()
540+
{
541+
$data = "";
542+
$data .= "00"; // answer: empty domain
543+
$data .= "00 29 03 e8"; // answer: type OPT, class 1000 UDP size
544+
$data .= "00 00 00 00"; // answer: ttl 0
545+
$data .= "00 00"; // answer: rdlength 0
546+
547+
$response = $this->parseAnswer($data);
548+
549+
$this->assertCount(1, $response->answers);
550+
$this->assertSame('', $response->answers[0]->name);
551+
$this->assertSame(Message::TYPE_OPT, $response->answers[0]->type);
552+
$this->assertSame(1000, $response->answers[0]->class);
553+
$this->assertSame(0, $response->answers[0]->ttl);
554+
$this->assertSame(array(), $response->answers[0]->data);
555+
}
556+
557+
public function testParseOptResponseWithCustomOptions()
558+
{
559+
$data = "";
560+
$data .= "00"; // answer: empty domain
561+
$data .= "00 29 03 e8"; // answer: type OPT, class 1000 UDP size
562+
$data .= "00 00 00 00"; // answer: ttl 0
563+
$data .= "00 0b"; // answer: rdlength 11
564+
$data .= "00 a0 00 03 66 6f 6f"; // OPT code 0xa0 encoded
565+
$data .= "00 01 00 00 "; // OPT code 0x01 encoded
566+
567+
$response = $this->parseAnswer($data);
568+
569+
$this->assertCount(1, $response->answers);
570+
$this->assertSame('', $response->answers[0]->name);
571+
$this->assertSame(Message::TYPE_OPT, $response->answers[0]->type);
572+
$this->assertSame(1000, $response->answers[0]->class);
573+
$this->assertSame(0, $response->answers[0]->ttl);
574+
$this->assertSame(array(0xa0 => 'foo', 0x01 => ''), $response->answers[0]->data);
575+
}
576+
539577
public function testParseSOAResponse()
540578
{
541579
$data = "";
@@ -957,6 +995,21 @@ public function testParseInvalidSSHFPResponseWhereRecordIsTooSmall()
957995
$this->parseAnswer($data);
958996
}
959997

998+
/**
999+
* @expectedException InvalidArgumentException
1000+
*/
1001+
public function testParseInvalidOPTResponseWhereRecordIsTooSmall()
1002+
{
1003+
$data = "";
1004+
$data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
1005+
$data .= "00 29 03 e8"; // answer: type OPT, 1000 bytes max size
1006+
$data .= "00 00 00 00"; // answer: ttl 0
1007+
$data .= "00 03"; // answer: rdlength 3
1008+
$data .= "00 00 00"; // answer: type 0, length incomplete
1009+
1010+
$this->parseAnswer($data);
1011+
}
1012+
9601013
/**
9611014
* @expectedException InvalidArgumentException
9621015
*/

0 commit comments

Comments
 (0)