diff --git a/src/lib/components/tools/NetworkVisualizer.svelte b/src/lib/components/tools/NetworkVisualizer.svelte index 1944c1b9..bac7417e 100644 --- a/src/lib/components/tools/NetworkVisualizer.svelte +++ b/src/lib/components/tools/NetworkVisualizer.svelte @@ -14,34 +14,44 @@ * Generates visual representation of network range */ function generateNetworkBlocks() { - const totalHosts = subnetInfo.hostCount; - const _usableHosts = subnetInfo.usableHosts; - - // For visualization, we'll show up to 256 blocks max + const { hostCount, cidr } = subnetInfo; const maxBlocks = 256; - const blocksToShow = Math.min(totalHosts, maxBlocks); - const blockSize = totalHosts > maxBlocks ? Math.ceil(totalHosts / maxBlocks) : 1; - - const blocks = []; - - for (let i = 0; i < blocksToShow; i++) { - const isNetwork = i === 0; - const isBroadcast = i === blocksToShow - 1 && totalHosts > 2; - const _isUsable = !isNetwork && !isBroadcast; - - blocks.push({ - id: i, - type: isNetwork ? 'network' : isBroadcast ? 'broadcast' : 'usable', - represents: blockSize, - tooltip: isNetwork - ? 'Network Address' - : isBroadcast - ? 'Broadcast Address' - : `Usable Host${blockSize > 1 ? 's' : ''}`, - }); - } + const blocksToShow = Math.min(hostCount, maxBlocks); + const blockSize = hostCount > maxBlocks ? Math.ceil(hostCount / maxBlocks) : 1; + + return Array.from({ length: blocksToShow }, (_, i) => { + const isFirst = i === 0; + const isLast = i === blocksToShow - 1; + + // RFC 3021: /31 and /32 have all IPs usable + if (cidr === 31) { + return { + id: i, + type: 'usable' as const, + represents: blockSize, + tooltip: isFirst ? 'Usable Host 1 (P2P)' : 'Usable Host 2 (P2P)', + }; + } + + if (cidr === 32) { + return { + id: i, + type: 'usable' as const, + represents: blockSize, + tooltip: 'Single Host', + }; + } + + // Normal subnets have network/broadcast reserved + const type = isFirst ? 'network' : isLast && hostCount > 2 ? 'broadcast' : 'usable'; + const tooltip = isFirst + ? 'Network Address' + : isLast && hostCount > 2 + ? 'Broadcast Address' + : `Usable Host${blockSize > 1 ? 's' : ''}`; - return blocks; + return { id: i, type, represents: blockSize, tooltip }; + }); } let networkBlocks = $derived(generateNetworkBlocks()); diff --git a/src/lib/utils/ip-calculations.ts b/src/lib/utils/ip-calculations.ts index ed092cbe..80cc6d5b 100644 --- a/src/lib/utils/ip-calculations.ts +++ b/src/lib/utils/ip-calculations.ts @@ -73,10 +73,21 @@ export function calculateSubnet(ip: string, cidr: number): SubnetInfo { const hostBits = 32 - cidr; const hostCount = Math.pow(2, hostBits); - const usableHosts = hostCount > 2 ? hostCount - 2 : 0; - const firstHost = numberToIP(ipToNumber(network.octets.join('.')) + 1); - const lastHost = numberToIP(ipToNumber(broadcast.octets.join('.')) - 1); + // RFC 3021: /31 point-to-point has both IPs usable, /32 is single host + const usableHosts = cidr === 32 ? 1 : cidr === 31 ? 2 : hostCount - 2; + + // For /31 and /32, all IPs are usable (no reserved network/broadcast) + let firstHost: IPAddress; + let lastHost: IPAddress; + + if (cidr >= 31) { + firstHost = network; + lastHost = cidr === 32 ? network : broadcast; + } else { + firstHost = numberToIP(ipToNumber(network.octets.join('.')) + 1); + lastHost = numberToIP(ipToNumber(broadcast.octets.join('.')) - 1); + } return { network, diff --git a/tests/api/internal-endpoints/ct-log-search.test.ts b/tests/api/internal-endpoints/ct-log-search.test.ts index bb14253c..5f7fab1f 100644 --- a/tests/api/internal-endpoints/ct-log-search.test.ts +++ b/tests/api/internal-endpoints/ct-log-search.test.ts @@ -33,7 +33,7 @@ describe('CT Log Search API', () => { expect(data.expiringSoon).toBeGreaterThanOrEqual(0); expect(data.wildcardCertificates).toBeGreaterThanOrEqual(0); expect(data.timestamp).toBeDefined(); - }); + }, 60000); // Increase timeout to 60s for slow external API it('should include required certificate fields', async () => { const { status, data } = await makeRequest('example.com'); @@ -123,6 +123,6 @@ describe('CT Log Search API', () => { expect(data.totalCertificates).toBe(0); expect(data.certificates).toHaveLength(0); expect(data.discoveredHostnames).toHaveLength(0); - }); + }, 60000); // Increase timeout to 60s for slow external API }); }); diff --git a/tests/api/subnetting.test.ts b/tests/api/subnetting.test.ts index 1fc44cf5..7078ad67 100644 --- a/tests/api/subnetting.test.ts +++ b/tests/api/subnetting.test.ts @@ -72,7 +72,7 @@ describe('Subnetting API Endpoints', () => { expect(ipObjectToString(result.broadcast)).toBe('192.168.1.1'); expect(ipObjectToString(result.subnet)).toBe('255.255.255.255'); expect(result.hostCount).toBe(1); - expect(result.usableHosts).toBe(0); + expect(result.usableHosts).toBe(1); // RFC 3021: /32 is a single usable host }); it('should handle invalid CIDR format', async () => { diff --git a/tests/unit/routes/subnetting/ipv4-subnet-calculator.test.ts b/tests/unit/routes/subnetting/ipv4-subnet-calculator.test.ts index a1615c4d..6d4f419f 100644 --- a/tests/unit/routes/subnetting/ipv4-subnet-calculator.test.ts +++ b/tests/unit/routes/subnetting/ipv4-subnet-calculator.test.ts @@ -51,10 +51,10 @@ describe('IPv4 Subnet Calculator Route Functionality', () => { expect(result.wildcardMask.octets).toEqual([0, 0, 0, 0]); expect(result.cidr).toBe(32); expect(result.hostCount).toBe(1); - expect(result.usableHosts).toBe(0); + expect(result.usableHosts).toBe(1); // Single host route }); - it('handles /31 point-to-point link correctly', () => { + it('handles /31 point-to-point link correctly (RFC 3021)', () => { const result = calculateSubnet('192.168.1.0', 31); expect(result.network.octets).toEqual([192, 168, 1, 0]); @@ -63,7 +63,7 @@ describe('IPv4 Subnet Calculator Route Functionality', () => { expect(result.wildcardMask.octets).toEqual([0, 0, 0, 1]); expect(result.cidr).toBe(31); expect(result.hostCount).toBe(2); - expect(result.usableHosts).toBe(0); // Traditional calculation: 2 hosts = 0 usable + expect(result.usableHosts).toBe(2); // RFC 3021: both IPs usable for point-to-point }); it('handles /28 network correctly', () => { diff --git a/tests/unit/utils/ip-calculations.test.ts b/tests/unit/utils/ip-calculations.test.ts index 4676b2df..584f2350 100644 --- a/tests/unit/utils/ip-calculations.test.ts +++ b/tests/unit/utils/ip-calculations.test.ts @@ -186,4 +186,63 @@ describe('IP calculations core logic', () => { }); }); }); -}); \ No newline at end of file + + describe('/31 point-to-point subnet (RFC 3021)', () => { + it('reports 2 usable hosts for /31 subnet', () => { + const subnet = calculateSubnetInfo('192.168.1.0', 31); + expect(subnet.usableHosts).toBe(2); + expect(subnet.hostCount).toBe(2); + }); + + it('uses network address as first usable host', () => { + const subnet = calculateSubnetInfo('10.0.0.0', 31); + expect(subnet.firstHost.octets).toEqual([10, 0, 0, 0]); + }); + + it('uses broadcast address as second usable host', () => { + const subnet = calculateSubnetInfo('10.0.0.0', 31); + expect(subnet.lastHost.octets).toEqual([10, 0, 0, 1]); + }); + + it('calculates correct network and broadcast for /31', () => { + const subnet = calculateSubnetInfo('172.16.0.10', 31); + // Network should be even address + expect(subnet.network.octets).toEqual([172, 16, 0, 10]); + // Broadcast should be odd address (network + 1) + expect(subnet.broadcast.octets).toEqual([172, 16, 0, 11]); + }); + + it('handles multiple /31 subnets in same range', () => { + const subnet1 = calculateSubnetInfo('192.168.1.0', 31); + const subnet2 = calculateSubnetInfo('192.168.1.2', 31); + const subnet3 = calculateSubnetInfo('192.168.1.4', 31); + + // First /31: .0 and .1 + expect(subnet1.firstHost.octets[3]).toBe(0); + expect(subnet1.lastHost.octets[3]).toBe(1); + + // Second /31: .2 and .3 + expect(subnet2.firstHost.octets[3]).toBe(2); + expect(subnet2.lastHost.octets[3]).toBe(3); + + // Third /31: .4 and .5 + expect(subnet3.firstHost.octets[3]).toBe(4); + expect(subnet3.lastHost.octets[3]).toBe(5); + }); + + it('reports 100% address utilization for /31', () => { + const subnet = calculateSubnetInfo('192.168.1.0', 31); + const utilization = (subnet.usableHosts / subnet.hostCount) * 100; + expect(utilization).toBe(100); + }); + + it('correctly identifies both IPs as usable in /31', () => { + const subnet = calculateSubnetInfo('203.0.113.0', 31); + + // Both the network and broadcast addresses are usable + expect(subnet.firstHost.octets).toEqual(subnet.network.octets); + expect(subnet.lastHost.octets).toEqual(subnet.broadcast.octets); + expect(subnet.usableHosts).toBe(2); + }); + }); +});