diff --git a/src/NetworkManager.ts b/src/NetworkManager.ts index 6fb38e4..4a26b91 100644 --- a/src/NetworkManager.ts +++ b/src/NetworkManager.ts @@ -20,7 +20,6 @@ export type IPAddress = IPv4Address | IPv6Address; export const enum IPFamily { IPv4 = "IPv4", - IPv4MappedIPv6 = "IPv4MappedIPv6", IPv6 = "IPv6", } diff --git a/src/coder/DNSLabelCoder.ts b/src/coder/DNSLabelCoder.ts index 2290800..4a383de 100644 --- a/src/coder/DNSLabelCoder.ts +++ b/src/coder/DNSLabelCoder.ts @@ -38,7 +38,7 @@ export class DNSLabelCoder { private static readonly NOT_POINTER_MASK = 0x3FFF; private static readonly NOT_POINTER_MASK_ONE_BYTE = 0x3F; - private buffer?: Buffer; + public buffer?: Buffer; readonly legacyUnicastEncoding: boolean; private startOfRR?: number; private startOfRData?: number; diff --git a/src/coder/ResourceRecord.spec.ts b/src/coder/ResourceRecord.spec.ts index d2471df..0515cfb 100644 --- a/src/coder/ResourceRecord.spec.ts +++ b/src/coder/ResourceRecord.spec.ts @@ -22,8 +22,8 @@ describe(ResourceRecord, () => { }); it("should encode IPv4-mapped IPv6 addresses in AAAA records", () => { - runRecordEncodingTest(new AAAARecord("test.local.", "::ffff:192.168.178.1")); - runRecordEncodingTest(new AAAARecord("sub.test.local.", "::ffff:192.168.0.1")); + runRecordEncodingTest(new AAAARecord("v4mapped.local.", "::ffff:192.168.178.1")); + runRecordEncodingTest(new AAAARecord("sub.v4mapped.local.", "::ffff:192.168.0.1")); }); it("should encode CNAME", () => { diff --git a/src/coder/ResourceRecord.ts b/src/coder/ResourceRecord.ts index 7aa1657..f298430 100644 --- a/src/coder/ResourceRecord.ts +++ b/src/coder/ResourceRecord.ts @@ -39,7 +39,6 @@ export abstract class ResourceRecord implements DNSRecord { // RFC 1035 4.1.3. ttl: number; flushFlag = false; - address?: AddressInfo; protected constructor(headerData: RecordRepresentation); protected constructor(name: string, type: RType, ttl?: number, flushFlag?: boolean, clazz?: RClass); diff --git a/src/coder/records/AAAARecord.ts b/src/coder/records/AAAARecord.ts index 552440f..3c5fc6c 100644 --- a/src/coder/records/AAAARecord.ts +++ b/src/coder/records/AAAARecord.ts @@ -7,7 +7,7 @@ import { RecordRepresentation, ResourceRecord } from "../ResourceRecord"; export class AAAARecord extends ResourceRecord { - public static readonly DEFAULT_TTL = 120; + public static readonly DEFAULT_TTL = AAAARecord.RR_DEFAULT_TTL_SHORT; readonly ipAddress: string; @@ -21,17 +21,10 @@ export class AAAARecord extends ResourceRecord { super(name); } - // Enhanced validation to check for IPv6 and IPv4-mapped IPv6 addresses - assert(net.isIPv6(ipAddress) || this.isIPv4MappedIPv6(ipAddress), "IP address is not in v6 or IPv4-mapped v6 format!"); + assert(net.isIPv6(ipAddress), "IP address is not in v6 format!"); this.ipAddress = ipAddress; } - // Utility method to check for IPv4-mapped IPv6 addresses - private isIPv4MappedIPv6(ipAddress: string): boolean { - //const ipv4MappedIPv6Regex = /^::ffff:(0{1,4}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])$/i; - //return ipv4MappedIPv6Regex.test(ipAddress); - return /^::ffff:(\d{1,3}\.){3}\d{1,3}$/i.test(ipAddress); - } - + protected getRDataEncodingLength(): number { return 16; // 16 byte ipv6 address } @@ -40,11 +33,11 @@ export class AAAARecord extends ResourceRecord { const oldOffset = offset; const address = enlargeIPv6(this.ipAddress); - const bytes = address.split(":"); - assert(bytes.length === 8, "invalid ip address"); + const hextets = address.split(":"); + assert(hextets.length === 8, "invalid IP address"); - for (const byte of bytes) { - const number = parseInt(byte, 16); + for (const hextet of hextets) { + const number = parseInt(hextet, 16); buffer.writeUInt16BE(number, offset); offset += 2; } diff --git a/src/coder/records/ARecord.ts b/src/coder/records/ARecord.ts index ebc9fae..8883e10 100644 --- a/src/coder/records/ARecord.ts +++ b/src/coder/records/ARecord.ts @@ -6,7 +6,7 @@ import { RecordRepresentation, ResourceRecord } from "../ResourceRecord"; export class ARecord extends ResourceRecord { - public static readonly DEFAULT_TTL = 120; + public static readonly DEFAULT_TTL = ARecord.RR_DEFAULT_TTL_SHORT; readonly ipAddress: string; @@ -20,14 +20,9 @@ export class ARecord extends ResourceRecord { super(name); } - // Adjust validation to accept IPv4-mapped IPv6 addresses - const isIPv4 = net.isIPv4(ipAddress); - //const isIPv4MappedIPv6 = /^::ffff:0{0,4}:((25[0-5]|(2[0-4]|1\d|\d)\d)\.){3}(25[0-5]|(2[0-4]|1\d|\d)\d)$/i.test(ipAddress); - const isIPv4MappedIPv6 = /^::ffff:(\d{1,3}\.){3}\d{1,3}$/i.test(ipAddress); - assert(isIPv4 || isIPv4MappedIPv6, "IP address is not in v4 or IPv4-mapped v6 format!"); + assert(net.isIPv4(ipAddress), "IP address is not in IPv4 format!"); - // Store the original IP address or convert IPv4-mapped IPv6 to IPv4 - this.ipAddress = isIPv4MappedIPv6 ? ipAddress.split(":").pop() as string : ipAddress; + this.ipAddress = ipAddress; } protected getRDataEncodingLength(): number { diff --git a/src/coder/test-utils.ts b/src/coder/test-utils.ts index 8b3be35..2729888 100644 --- a/src/coder/test-utils.ts +++ b/src/coder/test-utils.ts @@ -4,17 +4,9 @@ import { DNSPacket } from "./DNSPacket"; import { Question } from "./Question"; import { ResourceRecord } from "./ResourceRecord"; -// Utility function to convert IPv4-mapped IPv6 addresses to IPv4 -function convertIPv4MappedIPv6ToIPv4(address: string): string { - //const ipv4MappedIPv6Regex = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/; - //const match = address.match(ipv4MappedIPv6Regex); - //return match ? match[1] : address; - return address.replace(/^::ffff:/i, ""); -} - // Adjusted decodeContext to use the utility function for the address const decodeContext: AddressInfo = { - address: convertIPv4MappedIPv6ToIPv4("::ffff:0.0.0.0"), + address: "0.0.0.0", family: "ipv4", port: 5353, }; @@ -32,13 +24,13 @@ export function runRecordEncodingTest(record: Question | ResourceRecord, legacyU coder = new DNSLabelCoder(legacyUnicast); coder.initBuf(buffer); - // Adjusted to use the potentially converted address in decodeContext + + // test the decodeRecord method const decodedRecord = record instanceof Question ? Question.decode(decodeContext, coder, buffer, 0) : ResourceRecord.decode(decodeContext, coder, buffer, 0); expect(decodedRecord.readBytes).toBe(buffer.length); - // const record2 = decodedRecord.data!; expect(record2).toBeDefined(); diff --git a/src/util/domain-formatter.ts b/src/util/domain-formatter.ts index 3603477..df30151 100644 --- a/src/util/domain-formatter.ts +++ b/src/util/domain-formatter.ts @@ -2,6 +2,7 @@ import assert from "assert"; import net from "net"; import { ServiceType } from "../CiaoService"; import { Protocol } from "../index"; +import { getIPFromV4Mapped, isIPv4Mapped } from "./v4mapped"; function isProtocol(part: string): boolean { return part === "_" + Protocol.TCP || part === "_" + Protocol.UDP; @@ -130,18 +131,11 @@ export function removeTLD(hostname: string): string { return hostname.slice(0, lastDot); } -// Helper function to check if an address is an IPv4 bridged to IPv6 -function isIPv4BridgedToIPv6(address: string): boolean { - return /^::ffff:([0-9a-f]{8})$/i.test(address); -} - -// Add a new function to handle IPv4 bridged to IPv6 addresses -export function formatIPv4BridgedToIPv6(address: string): string { - if (!isIPv4BridgedToIPv6(address)) { +export function formatMappedIPv4Address(address: string): string { + if (!isIPv4Mapped(address)) { assert(net.isIPv4(address), "Illegal argument. Must be an IPv4 address!"); } - assert(net.isIPv4(address), "Illegal argument. Must be an IPv4 address!"); // Convert IPv4 address to its hexadecimal representation @@ -155,62 +149,17 @@ export function formatIPv4BridgedToIPv6(address: string): string { export function enlargeIPv6(address: string): string { assert(net.isIPv6(address), "Illegal argument. Must be ipv6 address!"); - // Handling IPv4-mapped IPv6 addresses - if (address.includes(".")) { - return address; // Return as is because it's an IPv4-mapped address - } - - const ipv4MappedIPv6Regex = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i; - const match = ipv4MappedIPv6Regex.exec(address); - if (match) { - return match[1]; // Return the IPv4 part - } - - let split = address.split(":"); - - if (split[0] === "") { - split.splice(0, 1); - - while (split.length < 8) { - split.unshift("0000"); - } - } else if (split[split.length - 1] === "") { - split.splice(split.length -1, 1); - - while (split.length < 8) { - split.push("0000"); - } - } else if (split.length < 8) { - let emptySection: number; - for (emptySection = 0; emptySection < split.length; emptySection++) { - if (split[emptySection] === "") { // find the first empty section - break; - } - } - - const replacements: string [] = new Array(9 - split.length).fill("0000"); - split.splice(emptySection, 1, ...replacements); - } - - for (let i = 0; i < split.length; i++) { - const element = split[i]; - if (element.length < 4) { - const zeros = new Array(4 - element.length).fill("0").join(""); - split.splice(i, 1, zeros + element); - } - } - // Find and expand zero compression "::" - const zeroCompressionIndex = split.indexOf(""); - if (zeroCompressionIndex !== -1) { - split.splice(zeroCompressionIndex, 1, ...Array(8 - split.length + 1).fill("0")); - } - - // Pad each segment with leading zeros - split = split.map(segment => segment.padStart(4, "0")); + const parts = address.split("::"); - const result = split.join(":"); - assert(split.length <= 8, `Resulting ipv6 address has more than 8 sections (${result})!`); - return result; + // Initialize head and tail arrays + const head = parts[0] ? parts[0].split(":") : []; + const tail = parts[1] ? parts[1].split(":") : []; + + // Calculate the number of groups to fill in with "0000" when we expand + const fill = new Array(8 - head.length - tail.length).fill("0000"); + + // Combine it all and normalize each hextet to be 4 characters long + return [...head, ...fill, ...tail].map(hextet => hextet.padStart(4, "0")).join(":"); } export function shortenIPv6(address: string | string[]): string { @@ -280,21 +229,22 @@ export function formatReverseAddressPTRName(address: string): string { const split = address.split(".").reverse(); return split.join(".") + ".in-addr.arpa"; - } else if (net.isIPv6(address) || isIPv4BridgedToIPv6(address)) { - if (isIPv4BridgedToIPv6(address)) { - // Convert IPv4-mapped IPv6 address to pure IPv6 format before processing - address = formatIPv4BridgedToIPv6(address.split("::ffff:")[1]); - } else { - address = enlargeIPv6(address).toUpperCase(); - } - - const nibbleSplit = address.replace(/:/g, "").split("").reverse(); - assert(nibbleSplit.length === 32, "Encountered invalid ipv6 address length! " + nibbleSplit.length); + } - return nibbleSplit.join(".") + ".ip6.arpa"; - } else { + if (!net.isIPv6(address)) { throw new Error("Supplied illegal ip address format: " + address); } + + if (isIPv4Mapped(address)) { + return (getIPFromV4Mapped(address) as string).split(".").reverse().join(".") + ".in-addr.arpa"; + } + + address = enlargeIPv6(address).toUpperCase(); + + const nibbleSplit = address.replace(/:/g, "").split("").reverse(); + assert(nibbleSplit.length === 32, "Encountered invalid ipv6 address length! " + nibbleSplit.length); + + return nibbleSplit.join(".") + ".ip6.arpa"; } export function ipAddressFromReversAddressName(name: string): string { diff --git a/src/util/v4mapped.spec.ts b/src/util/v4mapped.spec.ts new file mode 100644 index 0000000..0a82111 --- /dev/null +++ b/src/util/v4mapped.spec.ts @@ -0,0 +1,44 @@ +import { isIPv4Mapped } from "./v4mapped"; + +describe("isIPv4Mapped", () => { + test("should return true for valid IPv4-mapped IPv6 address", () => { + expect(isIPv4Mapped("::ffff:192.168.0.1")).toBe(true); + + // Test case-insensitivity + expect(isIPv4Mapped("::FFFF:127.0.0.1")).toBe(true); + }); + + test("should return false for non-IPv4-mapped IPv6 address", () => { + expect(isIPv4Mapped("2001:0db8:85a3:0000:0000:8a2e:0370:7334")).toBe(false); + expect(isIPv4Mapped("fe80::1ff:fe23:4567:890a")).toBe(false); + }); + + test("should return false for IPv4 address", () => { + expect(isIPv4Mapped("192.168.0.1")).toBe(false); + expect(isIPv4Mapped("127.0.0.1")).toBe(false); + }); + + test("should return false for invalid IPv4-mapped IPv6 address", () => { + expect(isIPv4Mapped("::ffff:999.999.999.999")).toBe(false); + expect(isIPv4Mapped("::ffff:192.168.0.256")).toBe(false); + expect(isIPv4Mapped("::ffff:192.168.0")).toBe(false); + expect(isIPv4Mapped("::ffff:192.168.0.1.1")).toBe(false); + }); + + test("should return false for malformed addresses", () => { + expect(isIPv4Mapped("::ffff:192.168.0.1g")).toBe(false); + expect(isIPv4Mapped("::ffff:192.168.0.")).toBe(false); + expect(isIPv4Mapped("::ffff:192.168..0.1")).toBe(false); + expect(isIPv4Mapped("::gggg:192.168.0.1")).toBe(false); + }); + + test("should return false for empty string", () => { + expect(isIPv4Mapped("")).toBe(false); + }); + + test("should return false for null or undefined", () => { + expect(isIPv4Mapped(null as unknown as string)).toBe(false); + expect(isIPv4Mapped(undefined as unknown as string)).toBe(false); + }); +}); + diff --git a/src/util/v4mapped.ts b/src/util/v4mapped.ts new file mode 100644 index 0000000..22d97ab --- /dev/null +++ b/src/util/v4mapped.ts @@ -0,0 +1,22 @@ +/** + * Test for the presence of an IPv4-mapped address embedded in an IPv6 address. + * + * @param address - IPv6 address + * @returns true if it is an IPv4-mapped address, false otherwise. + */ +export function isIPv4Mapped(address: string): boolean { + if(!/^::ffff:(\d{1,3}\.){3}\d{1,3}$/i.test(address)) { + return false; + } + + // Split the address apart into it's components and test for validity. + const parts = address.split(/::ffff:/i)[1]?.split(".").map(Number); + return parts?.length === 4 && parts.every(part => part >= 0 && part <= 255); +} + +export function getIPFromV4Mapped(address: string): string | null { + + // Split the address apart into it's components and test for validity. + return address.split(/^::ffff:/i)[1] ?? null; +} +