Skip to content
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
62 changes: 36 additions & 26 deletions src/lib/components/tools/NetworkVisualizer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
17 changes: 14 additions & 3 deletions src/lib/utils/ip-calculations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions tests/api/internal-endpoints/ct-log-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
});
});
2 changes: 1 addition & 1 deletion tests/api/subnetting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/routes/subnetting/ipv4-subnet-calculator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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', () => {
Expand Down
61 changes: 60 additions & 1 deletion tests/unit/utils/ip-calculations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,63 @@ describe('IP calculations core logic', () => {
});
});
});
});

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);
});
});
});
Loading