Skip to content

Commit

Permalink
Refactored support for IPv4-mapped addresses.
Browse files Browse the repository at this point in the history
  • Loading branch information
hjdhjd authored and donavanbecker committed Jul 5, 2024
1 parent e9a7328 commit 25a1215
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 114 deletions.
1 change: 0 additions & 1 deletion src/NetworkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export type IPAddress = IPv4Address | IPv6Address;

export const enum IPFamily {
IPv4 = "IPv4",
IPv4MappedIPv6 = "IPv4MappedIPv6",
IPv6 = "IPv6",
}

Expand Down
2 changes: 1 addition & 1 deletion src/coder/DNSLabelCoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/coder/ResourceRecord.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
1 change: 0 additions & 1 deletion src/coder/ResourceRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
21 changes: 7 additions & 14 deletions src/coder/records/AAAARecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
}
Expand All @@ -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;
}
Expand Down
11 changes: 3 additions & 8 deletions src/coder/records/ARecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {
Expand Down
14 changes: 3 additions & 11 deletions src/coder/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -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();

Expand Down
102 changes: 26 additions & 76 deletions src/util/domain-formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
44 changes: 44 additions & 0 deletions src/util/v4mapped.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

22 changes: 22 additions & 0 deletions src/util/v4mapped.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 25a1215

Please sign in to comment.