Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for IPv4-mapped IPv6 addresses #43

Merged
merged 6 commits into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to `@homebridge/ciao` will be documented in this file. This
### Added

- Add support for publishing on IPv6 networks (#19) (@adriancable)
- Add support for IPv4-mapped IPv6 addresses (#43) (@donavanbecker & @hjdhjd)

### Changed

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
5 changes: 5 additions & 0 deletions src/coder/ResourceRecord.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ describe(ResourceRecord, () => {
runRecordEncodingTest(new ARecord("sub.test.local.", "192.168.0.1"));
});

/*it("should encode IPv4-mapped IPv6 addresses in AAAA records", () => {
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", () => {
runRecordEncodingTest(new CNAMERecord("test.local.", "test2.local."));
runRecordEncodingTest(new CNAMERecord("sub.test.local.", "test2.local."));
Expand Down
12 changes: 6 additions & 6 deletions src/coder/records/AAAARecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ 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;

constructor(name: string, ipAddress: string, flushFlag?: boolean, ttl?: number, );
constructor(header: RecordRepresentation, ipAddress: string);
constructor(name: string | RecordRepresentation, ipAddress: string, flushFlag?: boolean, ttl?: number) {
if (typeof name === "string") {
super(name, RType.AAAA, ttl || AAAARecord.RR_DEFAULT_TTL_SHORT, flushFlag);
super(name, RType.AAAA, ttl || AAAARecord.DEFAULT_TTL, flushFlag);
} else {
assert(name.type === RType.AAAA);
super(name);
Expand All @@ -33,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
7 changes: 4 additions & 3 deletions src/coder/records/ARecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ 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;

constructor(name: string, ipAddress: string, flushFlag?: boolean, ttl?: number);
constructor(header: RecordRepresentation, ipAddress: string);
constructor(name: string | RecordRepresentation, ipAddress: string, flushFlag?: boolean, ttl?: number) {
if (typeof name === "string") {
super(name, RType.A, ttl || ARecord.RR_DEFAULT_TTL_SHORT, flushFlag);
super(name, RType.A, ttl || ARecord.DEFAULT_TTL, flushFlag);
} else {
assert(name.type === RType.A);
super(name);
}

assert(net.isIPv4(ipAddress), "IP address is not in v4 format!");
assert(net.isIPv4(ipAddress), "IP address is not in IPv4 format!");

this.ipAddress = ipAddress;
}

Expand Down
4 changes: 3 additions & 1 deletion src/coder/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DNSPacket } from "./DNSPacket";
import { Question } from "./Question";
import { ResourceRecord } from "./ResourceRecord";

// Adjusted decodeContext to use the utility function for the address
const decodeContext: AddressInfo = {
address: "0.0.0.0",
family: "ipv4",
Expand All @@ -23,12 +24,13 @@ export function runRecordEncodingTest(record: Question | ResourceRecord, legacyU
coder = new DNSLabelCoder(legacyUnicast);
coder.initBuf(buffer);


// 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
83 changes: 38 additions & 45 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,49 +131,35 @@ export function removeTLD(hostname: string): string {
return hostname.slice(0, lastDot);
}

export function enlargeIPv6(address: string): string {
assert(net.isIPv6(address), "Illegal argument. Must be ipv6 address!");

// we are not supporting ipv4-mapped ipv6 addresses here
assert(!address.includes("."), "ipv4-mapped ipv6 addresses are currently unsupported!");

const split = address.split(":");
export function formatMappedIPv4Address(address: string): string {
if (!isIPv4Mapped(address)) {
assert(net.isIPv4(address), "Illegal argument. Must be an IPv4 address!");
}

if (split[0] === "") {
split.splice(0, 1);
assert(net.isIPv4(address), "Illegal argument. Must be an IPv4 address!");

while (split.length < 8) {
split.unshift("0000");
}
} else if (split[split.length - 1] === "") {
split.splice(split.length -1, 1);
// Convert IPv4 address to its hexadecimal representation
const hexParts = address.split(".").map(part => parseInt(part).toString(16).padStart(2, "0"));
const ipv6Part = `::ffff:${hexParts.join("")}`;

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);
}
// Convert the hexadecimal representation to the standard IPv6 format
return ipv6Part.replace(/(.{4})(.{4})$/, "$1:$2");
}

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);
}
}
export function enlargeIPv6(address: string): string {
assert(net.isIPv6(address), "Illegal argument. Must be ipv6 address!");

const result = split.join(":");
assert(split.length <= 8, `Resulting ipv6 address has more than 8 sections (${result})!`);
return result;
const parts = address.split("::");

// 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 @@ -242,16 +229,22 @@ export function formatReverseAddressPTRName(address: string): string {
const split = address.split(".").reverse();

return split.join(".") + ".in-addr.arpa";
} else if (net.isIPv6(address)) {
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;
}