Skip to content

Commit 92f46e0

Browse files
committed
Fixes incorrect usable hosts for ipv4 subnet (#29)
1 parent b7c2652 commit 92f46e0

File tree

4 files changed

+122
-15
lines changed

4 files changed

+122
-15
lines changed

src/lib/components/tools/NetworkVisualizer.svelte

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
function generateNetworkBlocks() {
1717
const totalHosts = subnetInfo.hostCount;
1818
const _usableHosts = subnetInfo.usableHosts;
19+
const cidr = subnetInfo.cidr;
1920
2021
// For visualization, we'll show up to 256 blocks max
2122
const maxBlocks = 256;
@@ -24,20 +25,40 @@
2425
2526
const blocks = [];
2627
28+
// RFC 3021: /31 networks have no network/broadcast addresses
29+
const is31Subnet = cidr === 31;
30+
const is32Subnet = cidr === 32;
31+
2732
for (let i = 0; i < blocksToShow; i++) {
28-
const isNetwork = i === 0;
29-
const isBroadcast = i === blocksToShow - 1 && totalHosts > 2;
30-
const _isUsable = !isNetwork && !isBroadcast;
33+
let type: 'network' | 'broadcast' | 'usable';
34+
let tooltip: string;
35+
36+
if (is31Subnet) {
37+
// /31: Both IPs are usable hosts (point-to-point)
38+
type = 'usable';
39+
tooltip = i === 0 ? 'Usable Host 1 (P2P)' : 'Usable Host 2 (P2P)';
40+
} else if (is32Subnet) {
41+
// /32: Single host
42+
type = 'usable';
43+
tooltip = 'Single Host';
44+
} else {
45+
// Normal subnet: first is network, last is broadcast, rest are usable
46+
const isNetwork = i === 0;
47+
const isBroadcast = i === blocksToShow - 1 && totalHosts > 2;
48+
49+
type = isNetwork ? 'network' : isBroadcast ? 'broadcast' : 'usable';
50+
tooltip = isNetwork
51+
? 'Network Address'
52+
: isBroadcast
53+
? 'Broadcast Address'
54+
: `Usable Host${blockSize > 1 ? 's' : ''}`;
55+
}
3156
3257
blocks.push({
3358
id: i,
34-
type: isNetwork ? 'network' : isBroadcast ? 'broadcast' : 'usable',
59+
type,
3560
represents: blockSize,
36-
tooltip: isNetwork
37-
? 'Network Address'
38-
: isBroadcast
39-
? 'Broadcast Address'
40-
: `Usable Host${blockSize > 1 ? 's' : ''}`,
61+
tooltip,
4162
});
4263
}
4364

src/lib/utils/ip-calculations.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,37 @@ export function calculateSubnet(ip: string, cidr: number): SubnetInfo {
7373

7474
const hostBits = 32 - cidr;
7575
const hostCount = Math.pow(2, hostBits);
76-
const usableHosts = hostCount > 2 ? hostCount - 2 : 0;
7776

78-
const firstHost = numberToIP(ipToNumber(network.octets.join('.')) + 1);
79-
const lastHost = numberToIP(ipToNumber(broadcast.octets.join('.')) - 1);
77+
// Calculate usable hosts based on CIDR
78+
// RFC 3021: /31 has 2 usable hosts (point-to-point, no network/broadcast)
79+
// /32 has 1 usable host (single host route)
80+
// All others: total - 2 (excluding network and broadcast)
81+
let usableHosts: number;
82+
if (cidr === 32) {
83+
usableHosts = 1;
84+
} else if (cidr === 31) {
85+
usableHosts = 2; // Both IPs are usable for point-to-point links
86+
} else {
87+
usableHosts = hostCount - 2;
88+
}
89+
90+
// Calculate first and last host based on CIDR
91+
let firstHost: IPAddress;
92+
let lastHost: IPAddress;
93+
94+
if (cidr === 32) {
95+
// Host route - first and last are the same
96+
firstHost = network;
97+
lastHost = network;
98+
} else if (cidr === 31) {
99+
// Point-to-point - use network and broadcast as hosts (RFC 3021)
100+
firstHost = network;
101+
lastHost = broadcast;
102+
} else {
103+
// Normal subnet - exclude network and broadcast
104+
firstHost = numberToIP(ipToNumber(network.octets.join('.')) + 1);
105+
lastHost = numberToIP(ipToNumber(broadcast.octets.join('.')) - 1);
106+
}
80107

81108
return {
82109
network,

tests/unit/routes/subnetting/ipv4-subnet-calculator.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ describe('IPv4 Subnet Calculator Route Functionality', () => {
5151
expect(result.wildcardMask.octets).toEqual([0, 0, 0, 0]);
5252
expect(result.cidr).toBe(32);
5353
expect(result.hostCount).toBe(1);
54-
expect(result.usableHosts).toBe(0);
54+
expect(result.usableHosts).toBe(1); // Single host route
5555
});
5656

57-
it('handles /31 point-to-point link correctly', () => {
57+
it('handles /31 point-to-point link correctly (RFC 3021)', () => {
5858
const result = calculateSubnet('192.168.1.0', 31);
5959

6060
expect(result.network.octets).toEqual([192, 168, 1, 0]);
@@ -63,7 +63,7 @@ describe('IPv4 Subnet Calculator Route Functionality', () => {
6363
expect(result.wildcardMask.octets).toEqual([0, 0, 0, 1]);
6464
expect(result.cidr).toBe(31);
6565
expect(result.hostCount).toBe(2);
66-
expect(result.usableHosts).toBe(0); // Traditional calculation: 2 hosts = 0 usable
66+
expect(result.usableHosts).toBe(2); // RFC 3021: both IPs usable for point-to-point
6767
});
6868

6969
it('handles /28 network correctly', () => {

tests/unit/utils/ip-calculations.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,63 @@ describe('IP calculations core logic', () => {
186186
});
187187
});
188188
});
189+
190+
describe('/31 point-to-point subnet (RFC 3021)', () => {
191+
it('reports 2 usable hosts for /31 subnet', () => {
192+
const subnet = calculateSubnetInfo('192.168.1.0', 31);
193+
expect(subnet.usableHosts).toBe(2);
194+
expect(subnet.hostCount).toBe(2);
195+
});
196+
197+
it('uses network address as first usable host', () => {
198+
const subnet = calculateSubnetInfo('10.0.0.0', 31);
199+
expect(subnet.firstHost.octets).toEqual([10, 0, 0, 0]);
200+
});
201+
202+
it('uses broadcast address as second usable host', () => {
203+
const subnet = calculateSubnetInfo('10.0.0.0', 31);
204+
expect(subnet.lastHost.octets).toEqual([10, 0, 0, 1]);
205+
});
206+
207+
it('calculates correct network and broadcast for /31', () => {
208+
const subnet = calculateSubnetInfo('172.16.0.10', 31);
209+
// Network should be even address
210+
expect(subnet.network.octets).toEqual([172, 16, 0, 10]);
211+
// Broadcast should be odd address (network + 1)
212+
expect(subnet.broadcast.octets).toEqual([172, 16, 0, 11]);
213+
});
214+
215+
it('handles multiple /31 subnets in same range', () => {
216+
const subnet1 = calculateSubnetInfo('192.168.1.0', 31);
217+
const subnet2 = calculateSubnetInfo('192.168.1.2', 31);
218+
const subnet3 = calculateSubnetInfo('192.168.1.4', 31);
219+
220+
// First /31: .0 and .1
221+
expect(subnet1.firstHost.octets[3]).toBe(0);
222+
expect(subnet1.lastHost.octets[3]).toBe(1);
223+
224+
// Second /31: .2 and .3
225+
expect(subnet2.firstHost.octets[3]).toBe(2);
226+
expect(subnet2.lastHost.octets[3]).toBe(3);
227+
228+
// Third /31: .4 and .5
229+
expect(subnet3.firstHost.octets[3]).toBe(4);
230+
expect(subnet3.lastHost.octets[3]).toBe(5);
231+
});
232+
233+
it('reports 100% address utilization for /31', () => {
234+
const subnet = calculateSubnetInfo('192.168.1.0', 31);
235+
const utilization = (subnet.usableHosts / subnet.hostCount) * 100;
236+
expect(utilization).toBe(100);
237+
});
238+
239+
it('correctly identifies both IPs as usable in /31', () => {
240+
const subnet = calculateSubnetInfo('203.0.113.0', 31);
241+
242+
// Both the network and broadcast addresses are usable
243+
expect(subnet.firstHost.octets).toEqual(subnet.network.octets);
244+
expect(subnet.lastHost.octets).toEqual(subnet.broadcast.octets);
245+
expect(subnet.usableHosts).toBe(2);
246+
});
247+
});
189248
});

0 commit comments

Comments
 (0)