Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/DNS/Record.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public function setType($type): self
'TXT' => 16,
'AAAA' => 28,
'SRV' => 33,
'CAA' => 257,
];
$upper = strtoupper($type);
$this->type = $map[$upper] ?? 0;
Expand Down Expand Up @@ -321,6 +322,7 @@ public function getTypeName(): string
16 => 'TXT',
28 => 'AAAA',
33 => 'SRV',
257 => 'CAA',
];
return $types[$this->type] ?? "TYPE{$this->type}";
}
Expand Down
73 changes: 51 additions & 22 deletions src/DNS/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,35 +173,38 @@ public function start(): void
// Parse question domain
$domain = "";
$offset = 12;
while ($offset < \strlen($buffer)) {
// Get label length
$labelLength = \ord($buffer[$offset]);

while ($offset < strlen($buffer)) {
// Check for at least 1 byte for label length
if (($offset + 1) > strlen($buffer)) {
throw new \Exception('Malformed packet: not enough bytes for label length');
}
$labelLength = ord($buffer[$offset]);
Console::info("[PACKET] Processing label at offset {$offset}, length: {$labelLength}");

// End of question
if ($labelLength === 0 && \ord($buffer[$offset + 1]) === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From here, second part of statement ord(..) === 0 was removed, is this intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes because we now have new check above
if (($offset + 1) > strlen($buffer)) {

Console::info("[PACKET] End of domain name found at offset " . ($offset + 1));
// End of question (zero label)
if ($labelLength === 0) {
$offset += 1;
Console::info("[PACKET] End of domain name found at offset " . ($offset));
break;
}

// Extract label as string
$label = \substr($buffer, $offset + 1, $labelLength);
// Check for enough bytes for label
if (($offset + 1 + $labelLength) > strlen($buffer)) {
throw new \Exception('Malformed packet: not enough bytes for label');
}
$label = substr($buffer, $offset + 1, $labelLength);
Console::info("[PACKET] Found label: {$label}");

if (empty($domain)) {
$domain .= $label;
} else {
$domain .= '.' . $label;
}

// Skip to next label length
$offset += 1 + $labelLength;
}

// Parse question type
$unpacked = \unpack('ntype/nclass', \substr($buffer, $offset, 4));
// After domain, there should be 4 bytes for type/class
if (($offset + 4) > strlen($buffer)) {
throw new \Exception('Malformed packet: not enough bytes for question type/class');
}
$unpacked = unpack('ntype/nclass', substr($buffer, $offset, 4));
$offset += 4;
$typeByte = $unpacked['type'] ?? 0;
$classByte = $unpacked['class'] ?? 0;
Expand Down Expand Up @@ -270,23 +273,20 @@ public function start(): void

// Add answers section
foreach ($answers as $answer) {
$response .= \chr(192) . \chr(12); // 192 indicates this is pointer, 12 is offset to question.

$response .= chr(192) . chr(12); // 192 indicates this is pointer, 12 is offset to question.
// Pack the answer's type, not the question type
$response .= \pack('nn', $answer->getType(), $classByte);

$response .= pack('nn', $answer->getType(), $classByte);
/**
* @var string $type
*/
$type = $answer->getTypeName();

$response .= match ($type) {
'A' => $this->encodeIP($answer->getRdata(), $answer->getTTL()),
'AAAA' => $this->encodeIPv6($answer->getRdata(), $answer->getTTL()),
'CNAME' => $this->encodeDomain($answer->getRdata(), $answer->getTTL()),
'NS' => $this->encodeDomain($answer->getRdata(), $answer->getTTL()),
'TXT' => $this->encodeText($answer->getRdata(), $answer->getTTL()),
'CAA' => $this->encodeText($answer->getRdata(), $answer->getTTL()),
'CAA' => $this->encodeCAA($answer->getRdata(), $answer->getTTL()),
'MX' => $this->encodeMx($answer->getRdata(), $answer->getTTL(), $answer->getPriority() ?? 0),
'SRV' => $this->encodeSrv($answer->getRdata(), $answer->getTTL(), $answer->getPriority() ?? 0, $answer->getWeight() ?? 0, $answer->getPort() ?? 0),
default => ''
Expand Down Expand Up @@ -427,6 +427,35 @@ protected function encodeSrv(string $domain, int $ttl, int $priority, int $weigh
return $result;
}

protected function encodeCAA(array|string $rdata, int $ttl): string
{
// Accept either array or string for rdata
// If string, parse as 'tag value' (e.g. 'issue "letsencrypt.org"')
$flags = 0;
$tag = '';
$value = '';
if (is_array($rdata)) {
$flags = isset($rdata['flags']) ? (int)$rdata['flags'] : 0;
$tag = (string)($rdata['tag'] ?? 'issue');
$value = (string)($rdata['value'] ?? '');
} elseif (is_string($rdata)) {
// Try to parse: 'issue "letsencrypt.org"' or 'issuewild "example.com"'
if (preg_match('/^(issue|issuewild|iodef)\s+"([^"]+)"$/', $rdata, $m)) {
$tag = $m[1];
$value = $m[2];
} else {
// fallback: treat all as value
$tag = 'issue';
$value = $rdata;
}
}
$tagLen = strlen($tag);
$valueLen = strlen($value);
$rdataBin = chr($flags) . chr($tagLen) . $tag . $value;
$totalLen = 2 + $tagLen + $valueLen;
return pack('Nn', $ttl, $totalLen) . $rdataBin;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Categorize error messages into standardized error types
* to prevent high cardinality in telemetry metrics
Expand Down
Loading