feat(core): DHCP in the simulation core, as in Swift
engine.ts ports Swift's "DHCP server and automatic client": an active server listens on port 67 from the start and offers a static assignment before its range, round from the address after the last one offered; offers last 10 s, dynamic leases a day, and a server whose offer another server beat blacklists that address for a while. A client starts as an event: DISCOVER, OFFER, a check by ARP, DECLINE, REQUEST, ACK or NAK, 2.5 s per wait, and after ten failures its old address back. The snapshot carries the DHCP settings of clients and servers. Four scenarios match the Swift engine. One Swift error is named in the oracle test: a client run directly is advanced a second time, to FINISH, by the server's answer within its own DISCOVER, and the outer run then writes back its stale copy of the status (Swift's value types); the address and every trace agree, only the reported status is wrong in Swift. Swift's three DHCP engine tests are ported, with tests for stopping and the message format. 15 mutation probes, 14 caught; the other concerns the home router, which runs only once NAT is ported. Two departures from Java remain, to be fixed next: from the event queue the client's ARP check does not wait, so an address conflict is never found, and each answer that wakes the client makes it send its state's message again (a second DISCOVER and REQUEST, the second answered with NAK). Java waits a round trip and sends once per attempt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vendored
+8
-3
@@ -1,7 +1,12 @@
|
||||
// TextEncoder exists in every target (browsers, Node, workers) but belongs to
|
||||
// the DOM and Node type libraries, which this package does not load on
|
||||
// purpose. Declared here to the extent the core uses it.
|
||||
// TextEncoder and TextDecoder exist in every target (browsers, Node, workers)
|
||||
// but belong to the DOM and Node type libraries, which this package does not
|
||||
// load on purpose. Declared here to the extent the core uses them.
|
||||
|
||||
declare class TextEncoder {
|
||||
encode(input?: string): Uint8Array;
|
||||
}
|
||||
|
||||
declare class TextDecoder {
|
||||
constructor(label?: string, options?: { fatal?: boolean });
|
||||
decode(input?: Uint8Array): string;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
// "Deterministic runtime engine" (lines ~984–1284)
|
||||
// "Ethernet, ARP, IPv4, and ICMP packet runtime" (lines ~1284–3585,
|
||||
// without remote link, wireless and gateway NAT; with the firewall)
|
||||
// "UDP sockets and Router RIP" (lines ~4472–5187, the
|
||||
// UDP sockets; RIP not yet)
|
||||
// "Java-compatible TCP sockets" (lines ~5187–5913)
|
||||
// "DHCP server and automatic client" (lines ~5913–6694)
|
||||
// processScheduledRuntimeEvent, dropForGlobalPacketLoss (lines ~4753–4850)
|
||||
// Java counterparts: filius.hardware.Verbindung, filius.software.netzzugangsschicht,
|
||||
// filius.software.vermittlungsschicht (ARP, IP, ICMP), system.SwitchFirmware.
|
||||
// filius.software.vermittlungsschicht (ARP, IP, ICMP), system.SwitchFirmware,
|
||||
// filius.software.transportschicht (UDP, TCP), filius.software.dhcp.
|
||||
//
|
||||
// The Swift engine is a value type driven by a reducer; here it is a class
|
||||
// with the same inputs and outputs. Time is virtual: nothing sleeps, every
|
||||
@@ -14,26 +19,24 @@
|
||||
// Calls that block in Java (resolving an address, waiting for an echo reply)
|
||||
// advance the virtual clock inside the call, exactly as the Swift port does.
|
||||
//
|
||||
// "UDP sockets and Router RIP" (lines ~4472–5187, the
|
||||
// UDP sockets; RIP not yet)
|
||||
// "Java-compatible TCP sockets" (lines ~5187–5913)
|
||||
//
|
||||
// Not ported yet — each place throws NotPortedError instead of guessing:
|
||||
// gateway NAT, RIP routing, remote links. DHCP is not in the
|
||||
// snapshot yet, so Swift's DHCP handling of a datagram has nothing to do.
|
||||
// gateway NAT, RIP routing, remote links.
|
||||
|
||||
import { compareIds, type Id } from '../ids.js';
|
||||
import { isPcClassEndpoint, stableGeneratedMacAddress } from '../model/node.js';
|
||||
import type {
|
||||
DeviceConfiguration,
|
||||
DhcpServerConfiguration,
|
||||
FirewallConfiguration,
|
||||
FirewallRule,
|
||||
InterfaceConfiguration,
|
||||
InterfaceKey,
|
||||
ManualRoute,
|
||||
} from './config.js';
|
||||
import {
|
||||
ALL_PORTS,
|
||||
DEFAULT_SWITCH_RETENTION_MILLISECONDS,
|
||||
dhcpServerConfiguration,
|
||||
DIRECTLY_CONNECTED_SOURCE_MARKER,
|
||||
FIREWALL_PROTOCOL,
|
||||
firewallConfiguration,
|
||||
@@ -268,6 +271,67 @@ export interface TcpSession {
|
||||
|
||||
export type TcpConnectionResult = 'connected' | 'invalidSocket' | 'unreachable' | 'timedOut';
|
||||
|
||||
export const DHCP_SERVER_PORT = 67;
|
||||
export const DHCP_CLIENT_PORT = 68;
|
||||
export const DHCP_ROUND_TRIP_TIME_MILLISECONDS = 2_500;
|
||||
export const DHCP_OFFER_LIFETIME_MILLISECONDS = 4 * DHCP_ROUND_TRIP_TIME_MILLISECONDS;
|
||||
export const DHCP_DYNAMIC_LEASE_LIFETIME_MILLISECONDS = 24 * 60 * 60 * 1_000;
|
||||
export const DHCP_MAXIMUM_CLIENT_ERROR_COUNT = 10;
|
||||
|
||||
export type DhcpMessageType = 'DHCPDISCOVER' | 'DHCPREQUEST' | 'DHCPACK' | 'DHCPNAK' | 'DHCPOFFER' | 'DHCPDECLINE';
|
||||
const DHCP_MESSAGE_TYPES: readonly DhcpMessageType[] = ['DHCPDISCOVER', 'DHCPREQUEST', 'DHCPACK', 'DHCPNAK', 'DHCPOFFER', 'DHCPDECLINE'];
|
||||
|
||||
/** Swift's TopologyRuntimeDHCPMessage: lines of text in a UDP datagram. */
|
||||
export interface DhcpMessage {
|
||||
readonly type: DhcpMessageType;
|
||||
yourIPAddress: string;
|
||||
clientMacAddress?: string | undefined;
|
||||
subnetMask?: string | undefined;
|
||||
routerIPAddress?: string | undefined;
|
||||
dnsServerIPAddress?: string | undefined;
|
||||
serverIdentifier?: string | undefined;
|
||||
requestedIPAddress?: string | undefined;
|
||||
}
|
||||
|
||||
export interface DhcpOfferRecord {
|
||||
readonly clientMacAddress: string;
|
||||
readonly offeredIPAddress: string;
|
||||
readonly serverNodeId: Id;
|
||||
readonly expiresAtMilliseconds: number;
|
||||
}
|
||||
|
||||
export interface DhcpLeaseRecord {
|
||||
readonly clientMacAddress: string;
|
||||
readonly ipAddress: string;
|
||||
readonly serverNodeId: Id;
|
||||
/** Undefined for a static assignment. */
|
||||
readonly expiresAtMilliseconds: number | undefined;
|
||||
}
|
||||
|
||||
export type DhcpClientState = 'INIT' | 'DISCOVER' | 'VALIDATE' | 'DECLINE' | 'REQUEST' | 'ASSIGN_IP' | 'FINISH';
|
||||
|
||||
export interface DhcpClientStatus {
|
||||
state: DhcpClientState;
|
||||
errorCount: number;
|
||||
selectedServerIPAddress: string | undefined;
|
||||
offeredIPAddress: string | undefined;
|
||||
succeeded: boolean;
|
||||
}
|
||||
|
||||
export interface DhcpClientContext {
|
||||
readonly interfaceId: Id;
|
||||
readonly socketId: Id;
|
||||
readonly clientMacAddress: string;
|
||||
readonly oldDeviceConfiguration: DeviceConfiguration | undefined;
|
||||
readonly oldInterfaceConfiguration: InterfaceConfiguration | undefined;
|
||||
selectedOffer: DhcpMessage | undefined;
|
||||
}
|
||||
|
||||
/** Swift's caseInsensitiveCompare == .orderedSame, for addresses. */
|
||||
function sameIgnoringCase(lhs: string | undefined, rhs: string | undefined): boolean {
|
||||
return lhs !== undefined && rhs !== undefined && lhs.toLowerCase() === rhs.toLowerCase();
|
||||
}
|
||||
|
||||
export const TCP_MAXIMUM_SEGMENT_SIZE = 1_460;
|
||||
export const TCP_MAXIMUM_SEND_ATTEMPTS = 3;
|
||||
export const TCP_ROUND_TRIP_TIME_MILLISECONDS = 2_500;
|
||||
@@ -319,6 +383,13 @@ export interface RuntimeState {
|
||||
udpReceiveQueuesBySocketId: Map<Id, UdpReceivedDatagram[]>;
|
||||
tcpSessionsById: Map<Id, TcpSession>;
|
||||
tcpAcceptedSocketIdsByListenerId: Map<Id, Id[]>;
|
||||
dhcpOffersByIPAddress: Map<string, DhcpOfferRecord>;
|
||||
dhcpLeasesByIPAddress: Map<string, DhcpLeaseRecord>;
|
||||
dhcpBlacklistByServerNodeId: Map<Id, Map<string, number>>;
|
||||
dhcpLastOfferedAddressByServerNodeId: Map<Id, string>;
|
||||
dhcpServerSocketIdsByNodeId: Map<Id, Id>;
|
||||
dhcpClientStatusesByNodeId: Map<Id, DhcpClientStatus>;
|
||||
dhcpClientContextsByNodeId: Map<Id, DhcpClientContext>;
|
||||
nextSocketSequenceNumber: number;
|
||||
nextTcpInitialSequenceValue: number;
|
||||
nextEventSequenceNumber: number;
|
||||
@@ -349,6 +420,13 @@ function initialState(seed: bigint): RuntimeState {
|
||||
udpReceiveQueuesBySocketId: new Map(),
|
||||
tcpSessionsById: new Map(),
|
||||
tcpAcceptedSocketIdsByListenerId: new Map(),
|
||||
dhcpOffersByIPAddress: new Map(),
|
||||
dhcpLeasesByIPAddress: new Map(),
|
||||
dhcpBlacklistByServerNodeId: new Map(),
|
||||
dhcpLastOfferedAddressByServerNodeId: new Map(),
|
||||
dhcpServerSocketIdsByNodeId: new Map(),
|
||||
dhcpClientStatusesByNodeId: new Map(),
|
||||
dhcpClientContextsByNodeId: new Map(),
|
||||
nextSocketSequenceNumber: 1,
|
||||
nextTcpInitialSequenceValue: 1,
|
||||
nextEventSequenceNumber: 0,
|
||||
@@ -487,6 +565,7 @@ export class NetworkRuntimeEngine {
|
||||
state.udpReceiveQueuesBySocketId.clear();
|
||||
state.tcpSessionsById.clear();
|
||||
state.tcpAcceptedSocketIdsByListenerId.clear();
|
||||
this.clearDhcpState();
|
||||
state.firewallDecisions = [];
|
||||
return [this.output({ type: 'stopped' })];
|
||||
}
|
||||
@@ -658,8 +737,11 @@ export class NetworkRuntimeEngine {
|
||||
case 'ripBeacon':
|
||||
throw new NotPortedError('RIP beacons');
|
||||
case 'dhcpClientStart':
|
||||
this.runDhcpClient(kind.nodeId);
|
||||
return;
|
||||
case 'dhcpTimeout':
|
||||
throw new NotPortedError('DHCP client');
|
||||
this.handleDhcpClientTimeout(kind.nodeId);
|
||||
return;
|
||||
case 'tcpTimeout':
|
||||
this.handleTcpTimeout(kind.sessionId);
|
||||
return;
|
||||
@@ -895,6 +977,7 @@ export class NetworkRuntimeEngine {
|
||||
) {
|
||||
throw new NotPortedError('RIP advertisements');
|
||||
}
|
||||
this.processDhcpDatagram(packet, datagram, nodeId, networkInterface);
|
||||
const state = this.stateValue;
|
||||
const matching = [...state.socketsById.values()]
|
||||
.filter((socket) => socket.nodeId === nodeId && socket.protocol === 'udp' && socket.localPort === datagram.destinationPort)
|
||||
@@ -918,6 +1001,9 @@ export class NetworkRuntimeEngine {
|
||||
beforeHeaders: udpHeaders(datagram),
|
||||
detail: matching.length === 0 ? 'UDP destination port unbound' : 'UDP queued',
|
||||
});
|
||||
if (datagram.destinationPort === DHCP_CLIENT_PORT && state.dhcpClientContextsByNodeId.has(nodeId)) {
|
||||
this.advanceDhcpClient(nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// ── TCP sockets ──────────────────────────────────────────────────────────
|
||||
@@ -1544,8 +1630,542 @@ export class NetworkRuntimeEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private clearDhcpState(): void {
|
||||
const state = this.stateValue;
|
||||
state.dhcpOffersByIPAddress.clear();
|
||||
state.dhcpLeasesByIPAddress.clear();
|
||||
state.dhcpBlacklistByServerNodeId.clear();
|
||||
state.dhcpLastOfferedAddressByServerNodeId.clear();
|
||||
state.dhcpServerSocketIdsByNodeId.clear();
|
||||
state.dhcpClientStatusesByNodeId.clear();
|
||||
state.dhcpClientContextsByNodeId.clear();
|
||||
}
|
||||
|
||||
/** Every active server listens on port 67; every enabled client starts now. */
|
||||
private initializeDhcpState(): void {
|
||||
// DHCP (F2): no DHCP configuration exists in the snapshot yet.
|
||||
const state = this.stateValue;
|
||||
this.clearDhcpState();
|
||||
const snapshot = state.topologySnapshot;
|
||||
for (const node of snapshot.nodes) {
|
||||
if (snapshot.dhcpServerConfigurationsByNodeId.get(node.id)?.isActive !== true) continue;
|
||||
const serverInterface = this.dhcpServerInterface(node.id);
|
||||
if (serverInterface === undefined) continue;
|
||||
const socketId = this.bindUdpSocket(node.id, {
|
||||
localPort: DHCP_SERVER_PORT,
|
||||
localIPAddress: serverInterface.ipAddress,
|
||||
remoteIPAddress: LIMITED_BROADCAST_IP_ADDRESS,
|
||||
remotePort: DHCP_CLIENT_PORT,
|
||||
});
|
||||
if (socketId !== undefined) state.dhcpServerSocketIdsByNodeId.set(node.id, socketId);
|
||||
}
|
||||
for (const node of snapshot.nodes) {
|
||||
if (snapshot.dhcpClientConfigurationsByNodeId.get(node.id)?.isEnabled !== true) continue;
|
||||
if (!isPcClassEndpoint(node.kind) && node.kind !== 'gateway') continue;
|
||||
state.dhcpClientStatusesByNodeId.set(node.id, dhcpClientStatus());
|
||||
this.schedule(state.currentTimeMilliseconds, { kind: 'dhcpClientStart', nodeId: node.id });
|
||||
}
|
||||
}
|
||||
|
||||
// ── DHCP ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Starts (or goes on with) the node's DHCP client: its address set to
|
||||
* 0.0.0.0, a socket on port 68, then the client's states as far as the
|
||||
* answers so far allow.
|
||||
*/
|
||||
runDhcpClient(nodeId: Id): DhcpClientStatus | undefined {
|
||||
const state = this.stateValue;
|
||||
const node = this.nodeSnapshot(nodeId);
|
||||
if (
|
||||
state.phase !== 'running' ||
|
||||
state.topologySnapshot.dhcpClientConfigurationsByNodeId.get(nodeId)?.isEnabled !== true ||
|
||||
node === undefined ||
|
||||
(!isPcClassEndpoint(node.kind) && node.kind !== 'gateway')
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const clientInterface = this.dhcpClientInterface(nodeId);
|
||||
if (clientInterface === undefined) return undefined;
|
||||
if (state.dhcpClientContextsByNodeId.has(nodeId)) {
|
||||
this.advanceDhcpClient(nodeId);
|
||||
return state.dhcpClientStatusesByNodeId.get(nodeId);
|
||||
}
|
||||
const oldDeviceConfiguration = state.topologySnapshot.deviceConfigurations.get(nodeId);
|
||||
const oldInterfaceConfiguration = state.topologySnapshot.interfaceConfigurations.get(interfaceKey(nodeId, clientInterface.portId));
|
||||
this.setDhcpClientIPAddress(nodeId, clientInterface.portId, UNSPECIFIED_IP_ADDRESS);
|
||||
const resetInterface = this.networkInterfaces(nodeId).find((each) => each.portId === clientInterface.portId);
|
||||
const socketId =
|
||||
resetInterface === undefined
|
||||
? undefined
|
||||
: this.bindUdpSocket(nodeId, {
|
||||
localPort: DHCP_CLIENT_PORT,
|
||||
localIPAddress: resetInterface.ipAddress,
|
||||
remoteIPAddress: LIMITED_BROADCAST_IP_ADDRESS,
|
||||
remotePort: DHCP_SERVER_PORT,
|
||||
});
|
||||
if (resetInterface === undefined || socketId === undefined) {
|
||||
this.restoreDhcpClientOldIPAddress(nodeId, clientInterface.portId, oldDeviceConfiguration, oldInterfaceConfiguration);
|
||||
return undefined;
|
||||
}
|
||||
state.dhcpClientContextsByNodeId.set(nodeId, {
|
||||
interfaceId: clientInterface.portId,
|
||||
socketId,
|
||||
clientMacAddress: resetInterface.macAddress,
|
||||
oldDeviceConfiguration,
|
||||
oldInterfaceConfiguration,
|
||||
selectedOffer: undefined,
|
||||
});
|
||||
state.dhcpClientStatusesByNodeId.set(nodeId, dhcpClientStatus());
|
||||
this.advanceDhcpClient(nodeId);
|
||||
return state.dhcpClientStatusesByNodeId.get(nodeId);
|
||||
}
|
||||
|
||||
private advanceDhcpClient(nodeId: Id): void {
|
||||
const state = this.stateValue;
|
||||
for (;;) {
|
||||
const context = state.dhcpClientContextsByNodeId.get(nodeId);
|
||||
const status = state.dhcpClientStatusesByNodeId.get(nodeId);
|
||||
if (state.phase !== 'running' || context === undefined || status === undefined || status.state === 'FINISH') return;
|
||||
const backToDiscover = () => {
|
||||
status.errorCount += 1;
|
||||
status.state = 'DISCOVER';
|
||||
};
|
||||
switch (status.state) {
|
||||
case 'INIT':
|
||||
status.state = 'DISCOVER';
|
||||
break;
|
||||
case 'DISCOVER': {
|
||||
this.sendDhcpMessage(context.socketId, {
|
||||
type: 'DHCPDISCOVER',
|
||||
yourIPAddress: UNSPECIFIED_IP_ADDRESS,
|
||||
clientMacAddress: context.clientMacAddress,
|
||||
});
|
||||
const offer = this.receiveDhcpResponse(context.socketId, context.clientMacAddress, undefined, ['DHCPOFFER']);
|
||||
if (offer === undefined) {
|
||||
this.scheduleDhcpTimeout(nodeId);
|
||||
return;
|
||||
}
|
||||
context.selectedOffer = offer;
|
||||
status.selectedServerIPAddress = offer.serverIdentifier;
|
||||
status.offeredIPAddress = offer.yourIPAddress;
|
||||
status.state = 'VALIDATE';
|
||||
break;
|
||||
}
|
||||
case 'VALIDATE': {
|
||||
const offer = context.selectedOffer;
|
||||
if (offer === undefined) {
|
||||
backToDiscover();
|
||||
break;
|
||||
}
|
||||
const conflictMac = this.resolveMacAddress(nodeId, offer.yourIPAddress, {
|
||||
preferredInterfaceId: context.interfaceId,
|
||||
maxRetries: 1,
|
||||
selectInterfaceByAddress: false,
|
||||
});
|
||||
status.state = conflictMac === undefined ? 'REQUEST' : 'DECLINE';
|
||||
break;
|
||||
}
|
||||
case 'DECLINE': {
|
||||
const offer = context.selectedOffer;
|
||||
if (offer === undefined) {
|
||||
backToDiscover();
|
||||
break;
|
||||
}
|
||||
this.sendDhcpMessage(context.socketId, {
|
||||
type: 'DHCPDECLINE',
|
||||
yourIPAddress: UNSPECIFIED_IP_ADDRESS,
|
||||
clientMacAddress: context.clientMacAddress,
|
||||
serverIdentifier: offer.serverIdentifier,
|
||||
requestedIPAddress: offer.yourIPAddress,
|
||||
});
|
||||
context.selectedOffer = undefined;
|
||||
status.selectedServerIPAddress = undefined;
|
||||
status.offeredIPAddress = undefined;
|
||||
status.state = 'DISCOVER';
|
||||
break;
|
||||
}
|
||||
case 'REQUEST': {
|
||||
const offer = context.selectedOffer;
|
||||
const serverIdentifier = offer?.serverIdentifier;
|
||||
if (offer === undefined || serverIdentifier === undefined) {
|
||||
backToDiscover();
|
||||
break;
|
||||
}
|
||||
this.sendDhcpMessage(context.socketId, {
|
||||
type: 'DHCPREQUEST',
|
||||
yourIPAddress: UNSPECIFIED_IP_ADDRESS,
|
||||
clientMacAddress: context.clientMacAddress,
|
||||
serverIdentifier,
|
||||
requestedIPAddress: offer.yourIPAddress,
|
||||
});
|
||||
const response = this.receiveDhcpResponse(context.socketId, context.clientMacAddress, serverIdentifier, ['DHCPACK', 'DHCPNAK']);
|
||||
if (response === undefined) {
|
||||
this.scheduleDhcpTimeout(nodeId);
|
||||
return;
|
||||
}
|
||||
if (response.type === 'DHCPACK') {
|
||||
status.state = 'ASSIGN_IP';
|
||||
} else {
|
||||
context.selectedOffer = undefined;
|
||||
status.selectedServerIPAddress = undefined;
|
||||
status.offeredIPAddress = undefined;
|
||||
status.state = 'DISCOVER';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ASSIGN_IP': {
|
||||
const offer = context.selectedOffer;
|
||||
if (offer === undefined) {
|
||||
backToDiscover();
|
||||
break;
|
||||
}
|
||||
this.applyDhcpConfiguration(nodeId, context.interfaceId, offer);
|
||||
status.succeeded = true;
|
||||
status.state = 'FINISH';
|
||||
this.finishDhcpClient(nodeId, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleDhcpTimeout(nodeId: Id): void {
|
||||
this.removeDhcpTimeouts(nodeId);
|
||||
this.schedule(this.stateValue.currentTimeMilliseconds + DHCP_ROUND_TRIP_TIME_MILLISECONDS, { kind: 'dhcpTimeout', nodeId });
|
||||
}
|
||||
|
||||
private removeDhcpTimeouts(nodeId: Id): void {
|
||||
this.stateValue.pendingEvents = this.stateValue.pendingEvents.filter(
|
||||
(event) => !(event.kind.kind === 'dhcpTimeout' && event.kind.nodeId === nodeId),
|
||||
);
|
||||
}
|
||||
|
||||
private handleDhcpClientTimeout(nodeId: Id): void {
|
||||
const state = this.stateValue;
|
||||
const status = state.dhcpClientStatusesByNodeId.get(nodeId);
|
||||
if (!state.dhcpClientContextsByNodeId.has(nodeId) || status === undefined || status.state === 'FINISH') return;
|
||||
status.errorCount += 1;
|
||||
if (status.errorCount >= DHCP_MAXIMUM_CLIENT_ERROR_COUNT) {
|
||||
status.state = 'FINISH';
|
||||
status.succeeded = false;
|
||||
this.finishDhcpClient(nodeId, true);
|
||||
return;
|
||||
}
|
||||
this.advanceDhcpClient(nodeId);
|
||||
}
|
||||
|
||||
private finishDhcpClient(nodeId: Id, restoreOldIPAddress: boolean): void {
|
||||
const context = this.stateValue.dhcpClientContextsByNodeId.get(nodeId);
|
||||
if (context === undefined) return;
|
||||
this.stateValue.dhcpClientContextsByNodeId.delete(nodeId);
|
||||
if (restoreOldIPAddress) {
|
||||
this.restoreDhcpClientOldIPAddress(nodeId, context.interfaceId, context.oldDeviceConfiguration, context.oldInterfaceConfiguration);
|
||||
}
|
||||
this.closeSocket(context.socketId);
|
||||
this.removeDhcpTimeouts(nodeId);
|
||||
}
|
||||
|
||||
/** The server's part: answers a client's DISCOVER, REQUEST and DECLINE on its server interface. */
|
||||
private processDhcpDatagram(packet: IPv4Packet, datagram: UdpDatagram, nodeId: Id, receivingInterface: NetworkInterface): void {
|
||||
const state = this.stateValue;
|
||||
if (
|
||||
datagram.destinationPort !== DHCP_SERVER_PORT ||
|
||||
datagram.sourcePort !== DHCP_CLIENT_PORT ||
|
||||
state.topologySnapshot.dhcpServerConfigurationsByNodeId.get(nodeId)?.isActive !== true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const serverInterface = this.dhcpServerInterface(nodeId);
|
||||
if (serverInterface === undefined || serverInterface.portId !== receivingInterface.portId) return;
|
||||
const socketId = state.dhcpServerSocketIdsByNodeId.get(nodeId);
|
||||
if (socketId === undefined) return;
|
||||
const message = decodeDhcpMessage(datagram.payload);
|
||||
const clientMacAddress = message?.clientMacAddress;
|
||||
if (message === undefined || clientMacAddress === undefined) return;
|
||||
switch (message.type) {
|
||||
case 'DHCPDISCOVER': {
|
||||
const offeredIPAddress = this.offerDhcpAddress(nodeId, clientMacAddress);
|
||||
if (offeredIPAddress === undefined) return;
|
||||
const configuration = state.topologySnapshot.dhcpServerConfigurationsByNodeId.get(nodeId) ?? dhcpServerConfiguration();
|
||||
this.sendDhcpMessage(socketId, {
|
||||
type: 'DHCPOFFER',
|
||||
yourIPAddress: offeredIPAddress,
|
||||
clientMacAddress,
|
||||
subnetMask: serverInterface.subnetMask,
|
||||
routerIPAddress: this.determineDhcpGateway(nodeId, serverInterface, configuration),
|
||||
dnsServerIPAddress: this.determineDhcpDnsServer(nodeId, configuration),
|
||||
serverIdentifier: serverInterface.ipAddress,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'DHCPREQUEST': {
|
||||
const requestedIPAddress = message.requestedIPAddress;
|
||||
const selectedServer = message.serverIdentifier;
|
||||
if (requestedIPAddress === undefined || selectedServer === undefined) return;
|
||||
if (sameIgnoringCase(selectedServer, serverInterface.ipAddress)) {
|
||||
const accepted = this.requestDhcpAddress(nodeId, clientMacAddress, requestedIPAddress);
|
||||
this.sendDhcpMessage(socketId, {
|
||||
type: accepted ? 'DHCPACK' : 'DHCPNAK',
|
||||
yourIPAddress: requestedIPAddress,
|
||||
clientMacAddress,
|
||||
serverIdentifier: serverInterface.ipAddress,
|
||||
});
|
||||
} else {
|
||||
// Another server was chosen: this one keeps the address back for a while.
|
||||
getOrCreate(state.dhcpBlacklistByServerNodeId, nodeId, () => new Map()).set(
|
||||
requestedIPAddress.toLowerCase(),
|
||||
state.currentTimeMilliseconds + DHCP_OFFER_LIFETIME_MILLISECONDS,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'DHCPDECLINE':
|
||||
this.recordTrace({
|
||||
packetIdentity: packet.identity,
|
||||
nodeId,
|
||||
interfaceId: receivingInterface.portId,
|
||||
direction: 'local',
|
||||
layer: 'application',
|
||||
operation: 'accepted',
|
||||
detail: 'DHCPDECLINE ignored by Java server worker',
|
||||
});
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** A static assignment for the MAC, else the next free address of the range after the last one offered. */
|
||||
private offerDhcpAddress(serverNodeId: Id, clientMacAddress: string): string | undefined {
|
||||
const state = this.stateValue;
|
||||
const configuration = state.topologySnapshot.dhcpServerConfigurationsByNodeId.get(serverNodeId);
|
||||
if (configuration === undefined) return undefined;
|
||||
const staticAddress = configuration.staticAssignments.find((entry) => sameIgnoringCase(entry.macAddress, clientMacAddress))?.ipAddress;
|
||||
if (staticAddress !== undefined) return staticAddress;
|
||||
const lower = parseIPv4(configuration.lowerBoundIPAddress);
|
||||
const upper = parseIPv4(configuration.upperBoundIPAddress);
|
||||
if (lower === undefined || upper === undefined || lower > upper) return undefined;
|
||||
const last = state.dhcpLastOfferedAddressByServerNodeId.get(serverNodeId);
|
||||
const lastValue = last === undefined ? undefined : parseIPv4(last);
|
||||
const firstCandidate = lastValue === undefined ? lower : lastValue < upper ? lastValue + 1 : lower;
|
||||
let candidate = firstCandidate;
|
||||
do {
|
||||
const address = ipv4String(candidate);
|
||||
state.dhcpLastOfferedAddressByServerNodeId.set(serverNodeId, address);
|
||||
if (this.isDhcpAddressAvailable(serverNodeId, address)) {
|
||||
state.dhcpOffersByIPAddress.set(dhcpRecordKey(serverNodeId, address), {
|
||||
clientMacAddress,
|
||||
offeredIPAddress: address,
|
||||
serverNodeId,
|
||||
expiresAtMilliseconds: state.currentTimeMilliseconds + DHCP_OFFER_LIFETIME_MILLISECONDS,
|
||||
});
|
||||
return address;
|
||||
}
|
||||
candidate = candidate < upper ? candidate + 1 : lower;
|
||||
} while (candidate !== firstCandidate);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private requestDhcpAddress(serverNodeId: Id, clientMacAddress: string, requestedIPAddress: string): boolean {
|
||||
const state = this.stateValue;
|
||||
this.cleanupDhcpAssignments(serverNodeId);
|
||||
if (state.dhcpBlacklistByServerNodeId.get(serverNodeId)?.has(requestedIPAddress.toLowerCase()) === true) return false;
|
||||
const configuration = state.topologySnapshot.dhcpServerConfigurationsByNodeId.get(serverNodeId) ?? dhcpServerConfiguration();
|
||||
if (
|
||||
configuration.staticAssignments.some(
|
||||
(entry) => sameIgnoringCase(entry.macAddress, clientMacAddress) && sameIgnoringCase(entry.ipAddress, requestedIPAddress),
|
||||
)
|
||||
) {
|
||||
this.storeDhcpLease({ clientMacAddress, ipAddress: requestedIPAddress, serverNodeId, expiresAtMilliseconds: undefined });
|
||||
return true;
|
||||
}
|
||||
const key = dhcpRecordKey(serverNodeId, requestedIPAddress);
|
||||
const offer = state.dhcpOffersByIPAddress.get(key);
|
||||
let accepted: boolean;
|
||||
if (offer !== undefined) {
|
||||
if (!sameIgnoringCase(offer.clientMacAddress, clientMacAddress)) return false;
|
||||
state.dhcpOffersByIPAddress.delete(key);
|
||||
accepted = true;
|
||||
} else {
|
||||
accepted = this.isDhcpAddressAvailable(serverNodeId, requestedIPAddress);
|
||||
}
|
||||
if (!accepted) return false;
|
||||
this.storeDhcpLease({
|
||||
clientMacAddress,
|
||||
ipAddress: requestedIPAddress,
|
||||
serverNodeId,
|
||||
expiresAtMilliseconds: state.currentTimeMilliseconds + DHCP_DYNAMIC_LEASE_LIFETIME_MILLISECONDS,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A client's new lease replaces its earlier one at the same server. */
|
||||
private storeDhcpLease(lease: DhcpLeaseRecord): void {
|
||||
const leases = this.stateValue.dhcpLeasesByIPAddress;
|
||||
for (const [key, existing] of leases) {
|
||||
if (existing.serverNodeId === lease.serverNodeId && sameIgnoringCase(existing.clientMacAddress, lease.clientMacAddress)) {
|
||||
leases.delete(key);
|
||||
}
|
||||
}
|
||||
leases.set(dhcpRecordKey(lease.serverNodeId, lease.ipAddress), lease);
|
||||
}
|
||||
|
||||
private isDhcpAddressAvailable(serverNodeId: Id, ipAddress: string): boolean {
|
||||
const state = this.stateValue;
|
||||
this.cleanupDhcpAssignments(serverNodeId);
|
||||
const configuration = state.topologySnapshot.dhcpServerConfigurationsByNodeId.get(serverNodeId) ?? dhcpServerConfiguration();
|
||||
if (configuration.staticAssignments.some((entry) => sameIgnoringCase(entry.ipAddress, ipAddress))) return false;
|
||||
if (state.dhcpBlacklistByServerNodeId.get(serverNodeId)?.has(ipAddress.toLowerCase()) === true) return false;
|
||||
const key = dhcpRecordKey(serverNodeId, ipAddress);
|
||||
return !state.dhcpOffersByIPAddress.has(key) && !state.dhcpLeasesByIPAddress.has(key);
|
||||
}
|
||||
|
||||
/** Forgets the server's expired offers, dynamic leases and blacklist entries. */
|
||||
private cleanupDhcpAssignments(serverNodeId: Id): void {
|
||||
const state = this.stateValue;
|
||||
const now = state.currentTimeMilliseconds;
|
||||
for (const [key, offer] of state.dhcpOffersByIPAddress) {
|
||||
if (offer.serverNodeId === serverNodeId && offer.expiresAtMilliseconds < now) state.dhcpOffersByIPAddress.delete(key);
|
||||
}
|
||||
for (const [key, lease] of state.dhcpLeasesByIPAddress) {
|
||||
if (lease.serverNodeId === serverNodeId && lease.expiresAtMilliseconds !== undefined && lease.expiresAtMilliseconds < now) {
|
||||
state.dhcpLeasesByIPAddress.delete(key);
|
||||
}
|
||||
}
|
||||
const blacklist = state.dhcpBlacklistByServerNodeId.get(serverNodeId);
|
||||
if (blacklist !== undefined) {
|
||||
for (const [address, expiry] of blacklist) if (expiry < now) blacklist.delete(address);
|
||||
}
|
||||
}
|
||||
|
||||
/** The first queued answer of an accepted type for the MAC (and from the server, when given). */
|
||||
private receiveDhcpResponse(
|
||||
socketId: Id,
|
||||
clientMacAddress: string,
|
||||
serverIdentifier: string | undefined,
|
||||
acceptedTypes: readonly DhcpMessageType[],
|
||||
): DhcpMessage | undefined {
|
||||
for (let received = this.receiveUdp(socketId); received !== undefined; received = this.receiveUdp(socketId)) {
|
||||
const message = decodeDhcpMessage(received.datagram.payload);
|
||||
if (message === undefined || !acceptedTypes.includes(message.type) || !sameIgnoringCase(message.clientMacAddress, clientMacAddress)) {
|
||||
continue;
|
||||
}
|
||||
if (serverIdentifier !== undefined && !sameIgnoringCase(message.serverIdentifier, serverIdentifier)) continue;
|
||||
return message;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private sendDhcpMessage(socketId: Id, message: DhcpMessage): IPv4DeliveryResult | undefined {
|
||||
const socket = this.stateValue.socketsById.get(socketId);
|
||||
if (socket === undefined) return undefined;
|
||||
this.recordTrace({ nodeId: socket.nodeId, direction: 'outbound', layer: 'application', operation: 'sent', detail: message.type });
|
||||
return this.sendUdp(socketId, encodeDhcpMessage(message), {
|
||||
ipAddress: LIMITED_BROADCAST_IP_ADDRESS,
|
||||
port: socket.localPort === DHCP_SERVER_PORT ? DHCP_CLIENT_PORT : DHCP_SERVER_PORT,
|
||||
});
|
||||
}
|
||||
|
||||
/** A PC's or notebook's interface, a home router's LAN (index 1). */
|
||||
private dhcpServerInterface(nodeId: Id): NetworkInterface | undefined {
|
||||
const kind = this.nodeSnapshot(nodeId)?.kind;
|
||||
const interfaces = this.networkInterfaces(nodeId);
|
||||
if (kind === 'pc' || kind === 'notebook') return interfaces[0];
|
||||
if (kind === 'gateway') return interfaces.find((each) => each.index === 1);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** A PC's or notebook's interface, a home router's WAN (index 0). */
|
||||
private dhcpClientInterface(nodeId: Id): NetworkInterface | undefined {
|
||||
const kind = this.nodeSnapshot(nodeId)?.kind;
|
||||
const interfaces = this.networkInterfaces(nodeId);
|
||||
if (kind === 'pc' || kind === 'notebook') return interfaces[0];
|
||||
if (kind === 'gateway') return interfaces.find((each) => each.index === 0);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private determineDhcpGateway(serverNodeId: Id, serverInterface: NetworkInterface, configuration: DhcpServerConfiguration): string {
|
||||
if (this.nodeSnapshot(serverNodeId)?.kind === 'gateway') return serverInterface.ipAddress;
|
||||
if (configuration.useOwnSettings) return configuration.gatewayIPAddress;
|
||||
const gateway = this.stateValue.topologySnapshot.deviceConfigurations.get(serverNodeId)?.defaultGateway ?? '';
|
||||
return gateway === '' ? UNSPECIFIED_IP_ADDRESS : gateway;
|
||||
}
|
||||
|
||||
private determineDhcpDnsServer(serverNodeId: Id, configuration: DhcpServerConfiguration): string {
|
||||
if (this.nodeSnapshot(serverNodeId)?.kind === 'gateway' || configuration.useOwnSettings) return configuration.dnsServerIPAddress;
|
||||
const dns = this.stateValue.topologySnapshot.deviceConfigurations.get(serverNodeId)?.dnsServer ?? '';
|
||||
return dns === '' ? UNSPECIFIED_IP_ADDRESS : dns;
|
||||
}
|
||||
|
||||
private applyDhcpConfiguration(nodeId: Id, interfaceId: Id, offer: DhcpMessage): void {
|
||||
const node = this.nodeSnapshot(nodeId);
|
||||
if (node === undefined) return;
|
||||
const mask = offer.subnetMask ?? '0.0.0.0';
|
||||
const gateway = offer.routerIPAddress ?? UNSPECIFIED_IP_ADDRESS;
|
||||
const dns = offer.dnsServerIPAddress ?? UNSPECIFIED_IP_ADDRESS;
|
||||
const snapshot = this.stateValue.topologySnapshot;
|
||||
const devices = new Map(snapshot.deviceConfigurations);
|
||||
const interfaces = new Map(snapshot.interfaceConfigurations);
|
||||
if (isPcClassEndpoint(node.kind)) {
|
||||
devices.set(nodeId, { ipAddress: offer.yourIPAddress, subnetMask: mask, defaultGateway: gateway, dnsServer: dns });
|
||||
} else if (node.kind === 'gateway') {
|
||||
interfaces.set(interfaceKey(nodeId, interfaceId), { ipAddress: offer.yourIPAddress, subnetMask: mask });
|
||||
const current = devices.get(nodeId);
|
||||
devices.set(nodeId, {
|
||||
ipAddress: current?.ipAddress ?? '',
|
||||
subnetMask: current?.subnetMask ?? '',
|
||||
defaultGateway: gateway,
|
||||
dnsServer: dns,
|
||||
});
|
||||
}
|
||||
this.replaceTopologyConfigurations(devices, interfaces);
|
||||
}
|
||||
|
||||
private setDhcpClientIPAddress(nodeId: Id, interfaceId: Id, ipAddress: string): void {
|
||||
const node = this.nodeSnapshot(nodeId);
|
||||
if (node === undefined) return;
|
||||
const snapshot = this.stateValue.topologySnapshot;
|
||||
const devices = new Map(snapshot.deviceConfigurations);
|
||||
const interfaces = new Map(snapshot.interfaceConfigurations);
|
||||
const current = devices.get(nodeId);
|
||||
const currentInterface = interfaces.get(interfaceKey(nodeId, interfaceId));
|
||||
if (isPcClassEndpoint(node.kind) && current !== undefined) {
|
||||
devices.set(nodeId, { ...current, ipAddress });
|
||||
} else if (node.kind === 'gateway' && currentInterface !== undefined) {
|
||||
interfaces.set(interfaceKey(nodeId, interfaceId), { ipAddress, subnetMask: currentInterface.subnetMask });
|
||||
}
|
||||
this.replaceTopologyConfigurations(devices, interfaces);
|
||||
}
|
||||
|
||||
private restoreDhcpClientOldIPAddress(
|
||||
nodeId: Id,
|
||||
interfaceId: Id,
|
||||
deviceConfiguration: DeviceConfiguration | undefined,
|
||||
interfaceConfiguration: InterfaceConfiguration | undefined,
|
||||
): void {
|
||||
const node = this.nodeSnapshot(nodeId);
|
||||
if (node === undefined) return;
|
||||
const snapshot = this.stateValue.topologySnapshot;
|
||||
const devices = new Map(snapshot.deviceConfigurations);
|
||||
const interfaces = new Map(snapshot.interfaceConfigurations);
|
||||
const current = devices.get(nodeId);
|
||||
const currentInterface = interfaces.get(interfaceKey(nodeId, interfaceId));
|
||||
if (isPcClassEndpoint(node.kind) && deviceConfiguration !== undefined && current !== undefined) {
|
||||
devices.set(nodeId, { ...current, ipAddress: deviceConfiguration.ipAddress });
|
||||
} else if (node.kind === 'gateway' && interfaceConfiguration !== undefined && currentInterface !== undefined) {
|
||||
interfaces.set(interfaceKey(nodeId, interfaceId), {
|
||||
ipAddress: interfaceConfiguration.ipAddress,
|
||||
subnetMask: currentInterface.subnetMask,
|
||||
});
|
||||
}
|
||||
this.replaceTopologyConfigurations(devices, interfaces);
|
||||
}
|
||||
|
||||
private replaceTopologyConfigurations(
|
||||
deviceConfigurations: ReadonlyMap<Id, DeviceConfiguration>,
|
||||
interfaceConfigurations: ReadonlyMap<InterfaceKey, InterfaceConfiguration>,
|
||||
): void {
|
||||
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, { deviceConfigurations, interfaceConfigurations });
|
||||
}
|
||||
|
||||
private initializeNatState(): void {
|
||||
@@ -2864,6 +3484,77 @@ export function ipv4Headers(packet: IPv4Packet): HeaderField[] {
|
||||
];
|
||||
}
|
||||
|
||||
function dhcpClientStatus(): DhcpClientStatus {
|
||||
return { state: 'INIT', errorCount: 0, selectedServerIPAddress: undefined, offeredIPAddress: undefined, succeeded: false };
|
||||
}
|
||||
|
||||
function dhcpRecordKey(serverNodeId: Id, ipAddress: string): string {
|
||||
return `${serverNodeId.toLowerCase()}|${ipAddress.toLowerCase()}`;
|
||||
}
|
||||
|
||||
function ipv4String(address: number): string {
|
||||
return [24, 16, 8, 0].map((shift) => String((address >>> shift) & 0xff)).join('.');
|
||||
}
|
||||
|
||||
/** Swift's encodeDHCPMessage: the type, then `key=value` lines for the fields that are set. */
|
||||
export function encodeDhcpMessage(message: DhcpMessage): Uint8Array {
|
||||
const lines = [message.type, `yiaddr=${message.yourIPAddress}`];
|
||||
const add = (key: string, value: string | undefined) => {
|
||||
if (value !== undefined && value !== '') lines.push(`${key}=${value}`);
|
||||
};
|
||||
add('chaddr', message.clientMacAddress);
|
||||
add('router', message.routerIPAddress);
|
||||
add('subnetmask', message.subnetMask);
|
||||
add('dnsserver', message.dnsServerIPAddress);
|
||||
add('requested', message.requestedIPAddress);
|
||||
add('serverident', message.serverIdentifier);
|
||||
return new TextEncoder().encode(lines.join('\n'));
|
||||
}
|
||||
|
||||
/** Swift's decodeDHCPMessage: undefined for text that is not UTF-8 or of no known type. */
|
||||
export function decodeDhcpMessage(data: Uint8Array): DhcpMessage | undefined {
|
||||
let text: string;
|
||||
try {
|
||||
text = new TextDecoder('utf-8', { fatal: true }).decode(data);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const [command, ...fields] = text.split('\n');
|
||||
const type = DHCP_MESSAGE_TYPES.find((each) => sameIgnoringCase(each, command));
|
||||
if (type === undefined) return undefined;
|
||||
const message: DhcpMessage = { type, yourIPAddress: UNSPECIFIED_IP_ADDRESS };
|
||||
for (const line of fields) {
|
||||
const separator = line.indexOf('=');
|
||||
if (separator < 0) continue;
|
||||
const value = line.slice(separator + 1);
|
||||
switch (line.slice(0, separator)) {
|
||||
case 'yiaddr':
|
||||
message.yourIPAddress = value;
|
||||
break;
|
||||
case 'chaddr':
|
||||
message.clientMacAddress = value;
|
||||
break;
|
||||
case 'router':
|
||||
message.routerIPAddress = value;
|
||||
break;
|
||||
case 'subnetmask':
|
||||
message.subnetMask = value;
|
||||
break;
|
||||
case 'dnsserver':
|
||||
message.dnsServerIPAddress = value;
|
||||
break;
|
||||
case 'requested':
|
||||
message.requestedIPAddress = value;
|
||||
break;
|
||||
case 'serverident':
|
||||
message.serverIdentifier = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
function tcpSegmentRequiresAcknowledgement(segment: TcpSegment): boolean {
|
||||
return (
|
||||
(segment.flags & (TCP_FLAG.synchronize | TCP_FLAG.finish | TCP_FLAG.push)) !== 0 || segment.payload.length > 0
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
// State/TopologyNetworkRuntimeEngine.swift (TopologyNetworkRuntimeTopologySnapshot)
|
||||
// What the runtime starts from: the topology and every configuration, frozen
|
||||
// at the moment the simulation starts. Later protocol slices add their
|
||||
// configuration maps here (DHCP, NAT, remote link, wireless).
|
||||
// configuration maps here (NAT, remote link, wireless).
|
||||
|
||||
import { compareIds, type Id } from '../ids.js';
|
||||
import type { NodeKind } from '../model/node.js';
|
||||
import { canonicalMacAddress } from '../model/node.js';
|
||||
import type {
|
||||
DeviceConfiguration,
|
||||
DhcpClientConfiguration,
|
||||
DhcpServerConfiguration,
|
||||
FirewallConfiguration,
|
||||
InterfaceConfiguration,
|
||||
InterfaceKey,
|
||||
@@ -51,6 +53,8 @@ export interface TopologySnapshot {
|
||||
readonly interfaceConfigurations: ReadonlyMap<InterfaceKey, InterfaceConfiguration>;
|
||||
readonly manualRoutesByNodeId: ReadonlyMap<Id, readonly ManualRoute[]>;
|
||||
readonly ripEnabledByNodeId: ReadonlyMap<Id, boolean>;
|
||||
readonly dhcpClientConfigurationsByNodeId: ReadonlyMap<Id, DhcpClientConfiguration>;
|
||||
readonly dhcpServerConfigurationsByNodeId: ReadonlyMap<Id, DhcpServerConfiguration>;
|
||||
readonly firewallConfigurationsByNodeId: ReadonlyMap<Id, FirewallConfiguration>;
|
||||
readonly switchConfigurationsByNodeId: ReadonlyMap<Id, SwitchConfiguration>;
|
||||
}
|
||||
@@ -62,6 +66,8 @@ export interface TopologySnapshotInput {
|
||||
interfaceConfigurations?: ReadonlyMap<InterfaceKey, InterfaceConfiguration>;
|
||||
manualRoutesByNodeId?: ReadonlyMap<Id, readonly ManualRoute[]>;
|
||||
ripEnabledByNodeId?: ReadonlyMap<Id, boolean>;
|
||||
dhcpClientConfigurationsByNodeId?: ReadonlyMap<Id, DhcpClientConfiguration>;
|
||||
dhcpServerConfigurationsByNodeId?: ReadonlyMap<Id, DhcpServerConfiguration>;
|
||||
firewallConfigurationsByNodeId?: ReadonlyMap<Id, FirewallConfiguration>;
|
||||
switchConfigurationsByNodeId?: ReadonlyMap<Id, SwitchConfiguration>;
|
||||
}
|
||||
@@ -74,6 +80,8 @@ export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot
|
||||
interfaceConfigurations: input.interfaceConfigurations ?? new Map(),
|
||||
manualRoutesByNodeId: input.manualRoutesByNodeId ?? new Map(),
|
||||
ripEnabledByNodeId: input.ripEnabledByNodeId ?? new Map(),
|
||||
dhcpClientConfigurationsByNodeId: input.dhcpClientConfigurationsByNodeId ?? new Map(),
|
||||
dhcpServerConfigurationsByNodeId: input.dhcpServerConfigurationsByNodeId ?? new Map(),
|
||||
firewallConfigurationsByNodeId: input.firewallConfigurationsByNodeId ?? new Map(),
|
||||
switchConfigurationsByNodeId: input.switchConfigurationsByNodeId ?? new Map(),
|
||||
};
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
// Ported from filius-ipad ios/FiliusPadTests/TopologyNetworkRuntimeEngineTests.swift:
|
||||
// the event queue, the virtual clock, Java-compatible randomness, restart,
|
||||
// Ethernet/switch/ARP/IPv4/ICMP, routing errors, the firewall, packet loss
|
||||
// and trace retention, UDP and TCP sockets. The remaining Swift tests (DHCP,
|
||||
// DNS, RIP, NAT, remote link, packet viewer) follow with their protocol
|
||||
// and trace retention, UDP and TCP sockets, DHCP. The remaining Swift tests
|
||||
// (DNS, RIP, NAT, remote link, packet viewer) follow with their protocol
|
||||
// slices.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
decodeDhcpMessage,
|
||||
DHCP_DYNAMIC_LEASE_LIFETIME_MILLISECONDS,
|
||||
deviceConfiguration,
|
||||
dhcpServerConfiguration,
|
||||
encodeDhcpMessage,
|
||||
EMPTY_TOPOLOGY_SNAPSHOT,
|
||||
ETHERNET_BROADCAST_MAC_ADDRESS,
|
||||
firewallConfiguration,
|
||||
@@ -26,6 +31,8 @@ import {
|
||||
topologySnapshot,
|
||||
traceRetentionPolicy,
|
||||
UNSPECIFIED_MAC_ADDRESS,
|
||||
type DeviceConfiguration,
|
||||
type DhcpServerConfiguration,
|
||||
type EthernetFrame,
|
||||
type EthernetPayload,
|
||||
type FirewallConfiguration,
|
||||
@@ -212,6 +219,36 @@ describe('event queue and virtual clock', () => {
|
||||
expect(engine.bindUdpSocket(nodeId, { localPort: 7 })).toBe(uuid(1));
|
||||
});
|
||||
|
||||
it('forgets DHCP leases and client state on stop', () => {
|
||||
const [serverId, serverPortId, clientId, clientPortId] = [0x31, 0x32, 0x33, 0x34].map(uuid) as [string, string, string, string];
|
||||
const engine = startedEngine({
|
||||
nodes: [pc(serverId, serverPortId), pc(clientId, clientPortId)],
|
||||
links: [link(serverId, serverPortId, clientId, clientPortId)],
|
||||
deviceConfigurations: new Map([
|
||||
[serverId, device('10.4.0.1')],
|
||||
[clientId, device('0.0.0.0')],
|
||||
]),
|
||||
dhcpClientConfigurationsByNodeId: new Map([[clientId, { isEnabled: true }]]),
|
||||
dhcpServerConfigurationsByNodeId: new Map([
|
||||
[serverId, dhcpServerConfiguration({ isActive: true, lowerBoundIPAddress: '10.4.0.20', upperBoundIPAddress: '10.4.0.29' })],
|
||||
]),
|
||||
});
|
||||
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
|
||||
expect(engine.state.dhcpLeasesByIPAddress.size).toBe(1);
|
||||
|
||||
engine.handle({ type: 'stop' });
|
||||
const state = engine.state;
|
||||
expect([
|
||||
state.dhcpOffersByIPAddress.size,
|
||||
state.dhcpLeasesByIPAddress.size,
|
||||
state.dhcpBlacklistByServerNodeId.size,
|
||||
state.dhcpLastOfferedAddressByServerNodeId.size,
|
||||
state.dhcpServerSocketIdsByNodeId.size,
|
||||
state.dhcpClientStatusesByNodeId.size,
|
||||
state.dhcpClientContextsByNodeId.size,
|
||||
]).toEqual([0, 0, 0, 0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
it('forgets TCP sessions and accepted sockets on stop', () => {
|
||||
const [clientId, clientPortId, serverId, serverPortId] = [0x21, 0x22, 0x23, 0x24].map(uuid) as [string, string, string, string];
|
||||
const engine = startedEngine({
|
||||
@@ -867,3 +904,108 @@ describe('TCP sockets', () => {
|
||||
expect(engine.state.socketsById.get(listenerId)?.tcpState).toBe('LISTEN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DHCP', () => {
|
||||
const serverAndClient = (first: number, serverIp: string, client: DeviceConfiguration, server: Partial<DhcpServerConfiguration>) => {
|
||||
const [serverId, serverPortId, clientId, clientPortId] = [first, first + 1, first + 2, first + 3].map(uuid) as [string, string, string, string];
|
||||
const engine = startedEngine(
|
||||
{
|
||||
nodes: [pc(serverId, serverPortId), pc(clientId, clientPortId)],
|
||||
links: [link(serverId, serverPortId, clientId, clientPortId)],
|
||||
deviceConfigurations: new Map([
|
||||
[serverId, device(serverIp)],
|
||||
[clientId, client],
|
||||
]),
|
||||
dhcpClientConfigurationsByNodeId: new Map([[clientId, { isEnabled: true }]]),
|
||||
dhcpServerConfigurationsByNodeId: new Map([[serverId, dhcpServerConfiguration({ isActive: true, ...server })]]),
|
||||
},
|
||||
16,
|
||||
);
|
||||
return { engine, clientId, clientPortId };
|
||||
};
|
||||
|
||||
it('completes DISCOVER, OFFER, REQUEST and ACK and applies the configuration', () => {
|
||||
const { engine, clientId } = serverAndClient(0x1601, '192.168.50.1', device('169.254.1.2', { mask: '0.0.0.0' }), {
|
||||
lowerBoundIPAddress: '192.168.50.20',
|
||||
upperBoundIPAddress: '192.168.50.29',
|
||||
gatewayIPAddress: '192.168.50.1',
|
||||
dnsServerIPAddress: '192.168.50.53',
|
||||
useOwnSettings: true,
|
||||
});
|
||||
|
||||
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
|
||||
|
||||
expect(engine.state.topologySnapshot.deviceConfigurations.get(clientId)).toEqual(
|
||||
deviceConfiguration('192.168.50.20', '255.255.255.0', '192.168.50.1', '192.168.50.53'),
|
||||
);
|
||||
const status = engine.state.dhcpClientStatusesByNodeId.get(clientId);
|
||||
expect([status?.state, status?.errorCount, status?.succeeded]).toEqual(['FINISH', 0, true]);
|
||||
const leaseExpiry = [...engine.state.dhcpLeasesByIPAddress.values()][0]?.expiresAtMilliseconds;
|
||||
expect(leaseExpiry).toBeGreaterThan(engine.state.currentTimeMilliseconds);
|
||||
expect(leaseExpiry! - engine.state.currentTimeMilliseconds).toBeLessThanOrEqual(DHCP_DYNAMIC_LEASE_LIFETIME_MILLISECONDS);
|
||||
const details = engine.state.packetTraces.map((trace) => trace.detail);
|
||||
for (const message of ['DHCPDISCOVER', 'DHCPOFFER', 'DHCPREQUEST', 'DHCPACK']) expect(details).toContain(message);
|
||||
});
|
||||
|
||||
it('matches a static assignment regardless of case, with a lease that does not expire', () => {
|
||||
const clientPortId = uuid(0x1611 + 3);
|
||||
const { engine, clientId } = serverAndClient(0x1611, '10.0.0.1', device('0.0.0.0'), {
|
||||
lowerBoundIPAddress: '10.0.0.20',
|
||||
upperBoundIPAddress: '10.0.0.20',
|
||||
staticAssignments: [{ id: 'static', macAddress: stableMacAddress(clientPortId).toLowerCase(), ipAddress: '10.0.0.99' }],
|
||||
});
|
||||
|
||||
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
|
||||
|
||||
expect(engine.state.topologySnapshot.deviceConfigurations.get(clientId)?.ipAddress).toBe('10.0.0.99');
|
||||
expect([...engine.state.dhcpLeasesByIPAddress.values()][0]?.expiresAtMilliseconds).toBeUndefined();
|
||||
});
|
||||
|
||||
it('retries ten timeouts and gives back only the old address', () => {
|
||||
const clientId = uuid(0x1621);
|
||||
const oldConfiguration = deviceConfiguration('169.254.5.7', '255.255.0.0', '169.254.0.1', '169.254.0.53');
|
||||
const engine = startedEngine(
|
||||
{
|
||||
nodes: [pc(clientId, uuid(0x1622))],
|
||||
links: [],
|
||||
deviceConfigurations: new Map([[clientId, oldConfiguration]]),
|
||||
dhcpClientConfigurationsByNodeId: new Map([[clientId, { isEnabled: true }]]),
|
||||
},
|
||||
16,
|
||||
);
|
||||
|
||||
engine.handle({ type: 'advance', toMilliseconds: 0 });
|
||||
expect(engine.state.topologySnapshot.deviceConfigurations.get(clientId)?.ipAddress).toBe('0.0.0.0');
|
||||
expect(engine.state.dhcpClientStatusesByNodeId.get(clientId)?.state).toBe('DISCOVER');
|
||||
expect(engine.state.dhcpClientStatusesByNodeId.get(clientId)?.errorCount).toBe(0);
|
||||
|
||||
engine.handle({ type: 'advance', toMilliseconds: 25_000 });
|
||||
|
||||
const status = engine.state.dhcpClientStatusesByNodeId.get(clientId);
|
||||
expect([status?.state, status?.errorCount, status?.succeeded]).toEqual(['FINISH', 10, false]);
|
||||
expect(engine.state.topologySnapshot.deviceConfigurations.get(clientId)).toEqual(oldConfiguration);
|
||||
expect(engine.state.dhcpClientContextsByNodeId.has(clientId)).toBe(false);
|
||||
expect(engine.state.socketsById.size).toBe(0);
|
||||
});
|
||||
|
||||
it("writes and reads a message in Swift's text format", () => {
|
||||
const message = {
|
||||
type: 'DHCPOFFER' as const,
|
||||
yourIPAddress: '10.0.0.5',
|
||||
clientMacAddress: '02:00:00:00:00:01',
|
||||
subnetMask: '255.255.255.0',
|
||||
routerIPAddress: '10.0.0.1',
|
||||
dnsServerIPAddress: '10.0.0.53',
|
||||
requestedIPAddress: '',
|
||||
serverIdentifier: '10.0.0.1',
|
||||
};
|
||||
const text = new TextDecoder().decode(encodeDhcpMessage(message));
|
||||
expect(text).toBe(
|
||||
'DHCPOFFER\nyiaddr=10.0.0.5\nchaddr=02:00:00:00:00:01\nrouter=10.0.0.1\nsubnetmask=255.255.255.0\ndnsserver=10.0.0.53\nserverident=10.0.0.1',
|
||||
);
|
||||
const { requestedIPAddress: _empty, ...withoutEmpty } = message;
|
||||
expect(decodeDhcpMessage(new TextEncoder().encode(text))).toEqual(withoutEmpty);
|
||||
expect(decodeDhcpMessage(new TextEncoder().encode('dhcpack\nyiaddr=1.2.3.4\nnoise'))).toEqual({ type: 'DHCPACK', yourIPAddress: '1.2.3.4' });
|
||||
expect(decodeDhcpMessage(new TextEncoder().encode('HELLO'))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
deviceConfiguration,
|
||||
dhcpServerConfiguration,
|
||||
firewallConfiguration,
|
||||
firewallRule,
|
||||
interfaceKey,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
portSnapshot,
|
||||
switchConfiguration,
|
||||
topologySnapshot,
|
||||
type DhcpClientStatus,
|
||||
type FirewallConfiguration,
|
||||
type IPv4DeliveryResult,
|
||||
type IcmpObservation,
|
||||
@@ -55,6 +57,11 @@ interface Scenario {
|
||||
}[];
|
||||
}
|
||||
>;
|
||||
dhcpServers?: Record<
|
||||
string,
|
||||
{ active?: boolean; lower?: string; upper?: string; gateway?: string; dns?: string; own?: boolean; staticAssignments?: [string, string][] }
|
||||
>;
|
||||
dhcpClients?: string[];
|
||||
steps: {
|
||||
op: string;
|
||||
from?: string;
|
||||
@@ -144,6 +151,25 @@ function snapshotOf(scenario: Scenario): TopologySnapshot {
|
||||
),
|
||||
]),
|
||||
),
|
||||
dhcpServerConfigurationsByNodeId: new Map(
|
||||
Object.entries(scenario.dhcpServers ?? {}).map(([node, config]) => [
|
||||
node,
|
||||
dhcpServerConfiguration({
|
||||
isActive: config.active ?? false,
|
||||
lowerBoundIPAddress: config.lower ?? '0.0.0.0',
|
||||
upperBoundIPAddress: config.upper ?? '0.0.0.0',
|
||||
gatewayIPAddress: config.gateway ?? '0.0.0.0',
|
||||
dnsServerIPAddress: config.dns ?? '0.0.0.0',
|
||||
useOwnSettings: config.own ?? false,
|
||||
staticAssignments: (config.staticAssignments ?? []).map(([macAddress, ipAddress], index) => ({
|
||||
id: `static-${index}`,
|
||||
macAddress,
|
||||
ipAddress,
|
||||
})),
|
||||
}),
|
||||
]),
|
||||
),
|
||||
dhcpClientConfigurationsByNodeId: new Map((scenario.dhcpClients ?? []).map((node) => [node, { isEnabled: true }])),
|
||||
switchConfigurationsByNodeId: new Map(
|
||||
Object.entries(scenario.switches ?? {}).map(([node, config]) => [
|
||||
node,
|
||||
@@ -156,6 +182,16 @@ function snapshotOf(scenario: Scenario): TopologySnapshot {
|
||||
|
||||
const upper = (id: string) => id.toUpperCase();
|
||||
|
||||
function dhcpStatusJson(status: DhcpClientStatus) {
|
||||
return {
|
||||
state: status.state,
|
||||
errors: status.errorCount,
|
||||
server: status.selectedServerIPAddress ?? null,
|
||||
offered: status.offeredIPAddress ?? null,
|
||||
succeeded: status.succeeded,
|
||||
};
|
||||
}
|
||||
|
||||
function traceJson(event: TraceEvent): Record<string, unknown> {
|
||||
return {
|
||||
id: event.id,
|
||||
@@ -315,6 +351,11 @@ function run(scenario: Scenario): Record<string, unknown> {
|
||||
case 'cleanTcp':
|
||||
results.push(engine.cleanClosedTcpSocket(step.socket!));
|
||||
break;
|
||||
case 'runDhcp': {
|
||||
const status = engine.runDhcpClient(step.node!);
|
||||
results.push(status === undefined ? null : dhcpStatusJson(status));
|
||||
break;
|
||||
}
|
||||
case 'closeSocket':
|
||||
engine.closeSocket(step.socket!);
|
||||
results.push(!engine.state.socketsById.has(step.socket!));
|
||||
@@ -339,6 +380,25 @@ function run(scenario: Scenario): Record<string, unknown> {
|
||||
Object.fromEntries([...table].map(([mac, port]) => [mac, upper(port)])),
|
||||
),
|
||||
icmpObservations: mapOf(state.icmpObservationsByNodeId, (items) => items.map(observationJson)),
|
||||
devices: Object.fromEntries(
|
||||
[...state.topologySnapshot.deviceConfigurations].map(([node, device]) => [
|
||||
upper(node),
|
||||
[device.ipAddress, device.subnetMask, device.defaultGateway, device.dnsServer],
|
||||
]),
|
||||
),
|
||||
dhcp: {
|
||||
clients: mapOf(state.dhcpClientStatusesByNodeId, dhcpStatusJson),
|
||||
leases: [...state.dhcpLeasesByIPAddress]
|
||||
.sort(([lhs], [rhs]) => (lhs < rhs ? -1 : 1))
|
||||
.map(([key, lease]) => [key, lease.clientMacAddress, lease.ipAddress, lease.expiresAtMilliseconds ?? null]),
|
||||
offers: [...state.dhcpOffersByIPAddress]
|
||||
.sort(([lhs], [rhs]) => (lhs < rhs ? -1 : 1))
|
||||
.map(([key, offer]) => [key, offer.clientMacAddress, offer.expiresAtMilliseconds]),
|
||||
blacklist: mapOf(state.dhcpBlacklistByServerNodeId, (entries) =>
|
||||
[...entries].sort(([lhs], [rhs]) => (lhs < rhs ? -1 : 1)).map(([address, expiry]) => [address, expiry]),
|
||||
),
|
||||
servers: mapOf(state.dhcpServerSocketIdsByNodeId, upper),
|
||||
},
|
||||
sockets: [...state.socketsById.values()]
|
||||
.sort((lhs, rhs) => (upper(lhs.id) < upper(rhs.id) ? -1 : 1))
|
||||
.map((socket) => ({
|
||||
@@ -378,7 +438,35 @@ const scenarioNames = readdirSync(scenarioDirectory)
|
||||
.map((name) => name.replace('.scenario.json', ''))
|
||||
.sort();
|
||||
|
||||
/**
|
||||
* Where the Swift engine is wrong, per scenario: its result corrected.
|
||||
*
|
||||
* dhcp-lan: a DHCP client run directly (not from the event queue) sends its
|
||||
* DISCOVER at once; the server's OFFER arrives within that send, and the
|
||||
* UDP delivery advances the client again — to FINISH, with the address set.
|
||||
* The outer run then writes back its stale copy of the status (Swift's
|
||||
* value types): DISCOVER, not succeeded. The address and every trace are
|
||||
* the same; only the reported status is wrong in Swift.
|
||||
*/
|
||||
const SWIFT_ERRORS: Record<string, (expected: Record<string, unknown>) => Record<string, unknown>> = {
|
||||
'dhcp-lan': (expected) => {
|
||||
const client = '00000000-0000-0000-0000-000000000102';
|
||||
const finished = { errors: 0, offered: '192.168.0.102', server: '192.168.0.10', state: 'FINISH', succeeded: true };
|
||||
const results = [...(expected.results as unknown[])];
|
||||
results[1] = finished;
|
||||
const dhcp = expected.dhcp as { clients: Record<string, unknown> };
|
||||
return { ...expected, results, dhcp: { ...dhcp, clients: { ...dhcp.clients, [client]: finished } } };
|
||||
},
|
||||
};
|
||||
|
||||
describe('same result as the Swift engine', () => {
|
||||
it('corrects Swift only where it errs', () => {
|
||||
for (const [name, correct] of Object.entries(SWIFT_ERRORS)) {
|
||||
const expected = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.swift.json`), 'utf8')) as Record<string, unknown>;
|
||||
expect(JSON.stringify(correct(expected)), name).not.toBe(JSON.stringify(expected));
|
||||
}
|
||||
});
|
||||
|
||||
it('has scenarios with Swift results', () => {
|
||||
expect(scenarioNames.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -386,10 +474,8 @@ describe('same result as the Swift engine', () => {
|
||||
for (const name of scenarioNames) {
|
||||
it(name, () => {
|
||||
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.scenario.json`), 'utf8')) as Scenario;
|
||||
const expected = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.swift.json`), 'utf8')) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const swift = JSON.parse(readFileSync(join(scenarioDirectory, `${name}.swift.json`), 'utf8')) as Record<string, unknown>;
|
||||
const expected = SWIFT_ERRORS[name]?.(swift) ?? swift;
|
||||
const actual = run(scenario);
|
||||
|
||||
// Traces first and one by one: the first difference is the one that matters.
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"seed": 43,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000101",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000111",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000102",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000112",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000103",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000113",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000104",
|
||||
"kind": "networkSwitch",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000121",
|
||||
"label": "sw1"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000122",
|
||||
"label": "sw2"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000123",
|
||||
"label": "sw3"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000124",
|
||||
"label": "sw4"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F001",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000101",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000111",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000121"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F002",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000102",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000112",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000122"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F003",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000103",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000113",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000123"
|
||||
}
|
||||
],
|
||||
"devices": {
|
||||
"00000000-0000-0000-0000-000000000101": {
|
||||
"ip": "192.168.0.10",
|
||||
"mask": "255.255.255.0"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000102": {
|
||||
"ip": "169.254.1.2",
|
||||
"mask": "255.255.0.0"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000103": {
|
||||
"ip": "192.168.0.100",
|
||||
"mask": "255.255.255.0"
|
||||
}
|
||||
},
|
||||
"switches": {
|
||||
"00000000-0000-0000-0000-000000000104": {
|
||||
"ssid": "lab",
|
||||
"retention": 5000
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"op": "advance",
|
||||
"to": 60000
|
||||
}
|
||||
],
|
||||
"dhcpServers": {
|
||||
"00000000-0000-0000-0000-000000000101": {
|
||||
"active": true,
|
||||
"lower": "192.168.0.100",
|
||||
"upper": "192.168.0.100"
|
||||
}
|
||||
},
|
||||
"dhcpClients": [
|
||||
"00000000-0000-0000-0000-000000000102"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"seed": 41,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000101",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000111",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000102",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000112",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000103",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000113",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000104",
|
||||
"kind": "networkSwitch",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000121",
|
||||
"label": "sw1"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000122",
|
||||
"label": "sw2"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000123",
|
||||
"label": "sw3"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000124",
|
||||
"label": "sw4"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F001",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000101",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000111",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000121"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F002",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000102",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000112",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000122"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F003",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000103",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000113",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000123"
|
||||
}
|
||||
],
|
||||
"devices": {
|
||||
"00000000-0000-0000-0000-000000000101": {
|
||||
"ip": "192.168.0.10",
|
||||
"mask": "255.255.255.0"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000102": {
|
||||
"ip": "169.254.1.2",
|
||||
"mask": "255.255.0.0"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000103": {
|
||||
"ip": "169.254.1.3",
|
||||
"mask": "255.255.0.0"
|
||||
}
|
||||
},
|
||||
"switches": {
|
||||
"00000000-0000-0000-0000-000000000104": {
|
||||
"ssid": "lab",
|
||||
"retention": 5000
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"op": "advance",
|
||||
"to": 20000
|
||||
},
|
||||
{
|
||||
"op": "runDhcp",
|
||||
"node": "00000000-0000-0000-0000-000000000102"
|
||||
},
|
||||
{
|
||||
"op": "ping",
|
||||
"from": "00000000-0000-0000-0000-000000000102",
|
||||
"target": "192.168.0.10",
|
||||
"identifier": 1,
|
||||
"sequence": 1
|
||||
},
|
||||
{
|
||||
"op": "ping",
|
||||
"from": "00000000-0000-0000-0000-000000000101",
|
||||
"target": "192.168.0.150",
|
||||
"identifier": 2,
|
||||
"sequence": 1
|
||||
}
|
||||
],
|
||||
"dhcpServers": {
|
||||
"00000000-0000-0000-0000-000000000101": {
|
||||
"active": true,
|
||||
"lower": "192.168.0.100",
|
||||
"upper": "192.168.0.102",
|
||||
"gateway": "192.168.0.1",
|
||||
"dns": "192.168.0.53",
|
||||
"own": true,
|
||||
"staticAssignments": [
|
||||
[
|
||||
"02:00:00:00:01:13",
|
||||
"192.168.0.150"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"dhcpClients": [
|
||||
"00000000-0000-0000-0000-000000000102",
|
||||
"00000000-0000-0000-0000-000000000103"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"seed": 44,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000101",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000111",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000102",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000112",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000103",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000113",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000104",
|
||||
"kind": "networkSwitch",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000121",
|
||||
"label": "sw1"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000122",
|
||||
"label": "sw2"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000123",
|
||||
"label": "sw3"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000124",
|
||||
"label": "sw4"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F001",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000101",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000111",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000121"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F002",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000102",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000112",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000122"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F003",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000103",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000113",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000123"
|
||||
}
|
||||
],
|
||||
"devices": {
|
||||
"00000000-0000-0000-0000-000000000101": {
|
||||
"ip": "192.168.0.10",
|
||||
"mask": "255.255.255.0"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000102": {
|
||||
"ip": "169.254.1.2",
|
||||
"mask": "255.255.0.0"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000103": {
|
||||
"ip": "192.168.0.30",
|
||||
"mask": "255.255.255.0"
|
||||
}
|
||||
},
|
||||
"switches": {
|
||||
"00000000-0000-0000-0000-000000000104": {
|
||||
"ssid": "lab",
|
||||
"retention": 5000
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"op": "advance",
|
||||
"to": 10000
|
||||
},
|
||||
{
|
||||
"op": "runDhcp",
|
||||
"node": "00000000-0000-0000-0000-000000000102"
|
||||
},
|
||||
{
|
||||
"op": "advance",
|
||||
"to": 60000
|
||||
},
|
||||
{
|
||||
"op": "runDhcp",
|
||||
"node": "00000000-0000-0000-0000-000000000101"
|
||||
}
|
||||
],
|
||||
"dhcpClients": [
|
||||
"00000000-0000-0000-0000-000000000102"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"seed": 42,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000101",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000111",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000102",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000112",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000103",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000113",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000104",
|
||||
"kind": "networkSwitch",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000121",
|
||||
"label": "sw1"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000122",
|
||||
"label": "sw2"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000123",
|
||||
"label": "sw3"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000124",
|
||||
"label": "sw4"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F001",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000101",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000111",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000121"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F002",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000102",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000112",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000122"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F003",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000103",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000113",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000104",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000123"
|
||||
}
|
||||
],
|
||||
"devices": {
|
||||
"00000000-0000-0000-0000-000000000101": {
|
||||
"ip": "192.168.0.10",
|
||||
"mask": "255.255.255.0",
|
||||
"gateway": "192.168.0.254"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000102": {
|
||||
"ip": "169.254.1.2",
|
||||
"mask": "255.255.0.0"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000103": {
|
||||
"ip": "192.168.0.30",
|
||||
"mask": "255.255.255.0"
|
||||
}
|
||||
},
|
||||
"switches": {
|
||||
"00000000-0000-0000-0000-000000000104": {
|
||||
"ssid": "lab",
|
||||
"retention": 5000
|
||||
}
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"op": "advance",
|
||||
"to": 20000
|
||||
}
|
||||
],
|
||||
"dhcpServers": {
|
||||
"00000000-0000-0000-0000-000000000101": {
|
||||
"active": true,
|
||||
"lower": "192.168.0.100",
|
||||
"upper": "192.168.0.110"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000103": {
|
||||
"active": true,
|
||||
"lower": "192.168.0.200",
|
||||
"upper": "192.168.0.210",
|
||||
"own": true,
|
||||
"gateway": "192.168.0.1",
|
||||
"dns": "8.8.8.8"
|
||||
}
|
||||
},
|
||||
"dhcpClients": [
|
||||
"00000000-0000-0000-0000-000000000102"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,49 @@
|
||||
"10.1.0.2" : "02:00:00:00:04:11"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000401" : [
|
||||
"10.1.0.2",
|
||||
"255.255.255.0",
|
||||
"10.1.0.1",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000402" : [
|
||||
"10.1.0.1",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000403" : [
|
||||
"10.2.0.2",
|
||||
"255.255.255.0",
|
||||
"10.2.0.1",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000404" : [
|
||||
"10.1.0.3",
|
||||
"255.255.255.0",
|
||||
"10.1.0.1",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
{
|
||||
"accepted" : false,
|
||||
|
||||
@@ -11,6 +11,43 @@
|
||||
"20.0.0.1" : "02:00:00:00:02:22"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000201" : [
|
||||
"10.0.0.2",
|
||||
"255.255.255.0",
|
||||
"10.0.0.1",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000202" : [
|
||||
"10.0.0.1",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000203" : [
|
||||
"20.0.0.2",
|
||||
"255.255.255.0",
|
||||
"20.0.0.1",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
{
|
||||
"accepted" : true,
|
||||
|
||||
@@ -15,6 +15,55 @@
|
||||
"172.16.0.1" : "02:00:00:00:03:32"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000301" : [
|
||||
"192.168.1.10",
|
||||
"255.255.255.0",
|
||||
"192.168.1.1",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000302" : [
|
||||
"192.168.1.1",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000303" : [
|
||||
"10.10.10.2",
|
||||
"255.255.255.0",
|
||||
"10.10.10.1",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000304" : [
|
||||
"172.16.0.10",
|
||||
"255.255.0.0",
|
||||
"172.16.0.1",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000305" : [
|
||||
"172.16.0.20",
|
||||
"255.255.0.0",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
{
|
||||
"accepted" : true,
|
||||
|
||||
@@ -7,6 +7,37 @@
|
||||
"10.5.0.1" : "02:00:00:00:05:11"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000501" : [
|
||||
"10.5.0.1",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000502" : [
|
||||
"10.5.0.2",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
|
||||
],
|
||||
|
||||
@@ -13,6 +13,43 @@
|
||||
"192.168.0.20" : "02:00:00:00:01:12"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000101" : [
|
||||
"192.168.0.10",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102" : [
|
||||
"192.168.0.20",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103" : [
|
||||
"192.168.0.30",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
|
||||
],
|
||||
|
||||
@@ -10,6 +10,43 @@
|
||||
"192.168.0.20" : "02:00:00:00:01:12"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000101" : [
|
||||
"192.168.0.10",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102" : [
|
||||
"192.168.0.20",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103" : [
|
||||
"192.168.0.30",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
|
||||
],
|
||||
|
||||
@@ -13,6 +13,43 @@
|
||||
"192.168.0.20" : "02:00:00:00:01:12"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000101" : [
|
||||
"192.168.0.10",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102" : [
|
||||
"192.168.0.20",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103" : [
|
||||
"192.168.0.30",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
|
||||
],
|
||||
|
||||
@@ -11,6 +11,43 @@
|
||||
"20.0.0.1" : "02:00:00:00:02:22"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000201" : [
|
||||
"10.0.0.2",
|
||||
"255.255.255.0",
|
||||
"10.0.0.1",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000202" : [
|
||||
"10.0.0.1",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000203" : [
|
||||
"20.0.0.2",
|
||||
"255.255.255.0",
|
||||
"20.0.0.1",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
{
|
||||
"accepted" : true,
|
||||
|
||||
@@ -11,6 +11,43 @@
|
||||
"192.168.0.20" : "02:00:00:00:01:12"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000101" : [
|
||||
"192.168.0.10",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000102" : [
|
||||
"192.168.0.20",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000103" : [
|
||||
"192.168.0.30",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
|
||||
],
|
||||
|
||||
@@ -11,6 +11,43 @@
|
||||
"20.0.0.1" : "02:00:00:00:02:22"
|
||||
}
|
||||
},
|
||||
"devices" : {
|
||||
"00000000-0000-0000-0000-000000000201" : [
|
||||
"10.0.0.2",
|
||||
"255.255.255.0",
|
||||
"10.0.0.1",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000202" : [
|
||||
"10.0.0.1",
|
||||
"255.255.255.0",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000203" : [
|
||||
"20.0.0.2",
|
||||
"255.255.255.0",
|
||||
"20.0.0.1",
|
||||
""
|
||||
]
|
||||
},
|
||||
"dhcp" : {
|
||||
"blacklist" : {
|
||||
|
||||
},
|
||||
"clients" : {
|
||||
|
||||
},
|
||||
"leases" : [
|
||||
|
||||
],
|
||||
"offers" : [
|
||||
|
||||
],
|
||||
"servers" : {
|
||||
|
||||
}
|
||||
},
|
||||
"firewallDecisions" : [
|
||||
{
|
||||
"accepted" : true,
|
||||
|
||||
Reference in New Issue
Block a user