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:
@@ -42,6 +42,12 @@ struct ScenarioStep: Decodable {
|
||||
// TCP: the payload repeated this many times, for messages longer than a segment.
|
||||
let repeatCount: Int?; let localPort: UInt16?
|
||||
}
|
||||
struct ScenarioDHCPServer: Decodable {
|
||||
let active: Bool?; let lower: String?; let upper: String?
|
||||
let gateway: String?; let dns: String?; let own: Bool?
|
||||
/// Pairs of MAC and IP address.
|
||||
let staticAssignments: [[String]]?
|
||||
}
|
||||
struct Scenario: Decodable {
|
||||
let seed: UInt64
|
||||
let speedPercent: Int?
|
||||
@@ -52,6 +58,8 @@ struct Scenario: Decodable {
|
||||
let routes: [String: [ScenarioRoute]]?
|
||||
let switches: [String: ScenarioSwitch]?
|
||||
let firewalls: [String: ScenarioFirewall]?
|
||||
let dhcpServers: [String: ScenarioDHCPServer]?
|
||||
let dhcpClients: [String]?
|
||||
let steps: [ScenarioStep]
|
||||
}
|
||||
|
||||
@@ -138,10 +146,26 @@ func snapshot(of scenario: Scenario) -> TopologyNetworkRuntimeTopologySnapshot {
|
||||
}
|
||||
)
|
||||
}
|
||||
var dhcpServers: [UUID: TopologyDHCPServerConfiguration] = [:]
|
||||
for (node, config) in scenario.dhcpServers ?? [:] {
|
||||
dhcpServers[uuid(node)] = TopologyDHCPServerConfiguration(
|
||||
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 {
|
||||
TopologyDHCPStaticAssignment(macAddress: $0[0], ipAddress: $0[1])
|
||||
}
|
||||
)
|
||||
}
|
||||
var dhcpClients: [UUID: TopologyDHCPClientConfiguration] = [:]
|
||||
for node in scenario.dhcpClients ?? [] { dhcpClients[uuid(node)] = TopologyDHCPClientConfiguration(isEnabled: true) }
|
||||
return TopologyNetworkRuntimeTopologySnapshot(
|
||||
nodes: nodes, links: links,
|
||||
deviceConfigurations: devices, interfaceConfigurations: interfaces,
|
||||
manualRoutesByNodeID: routes,
|
||||
dhcpClientConfigurationsByNodeID: dhcpClients,
|
||||
dhcpServerConfigurationsByNodeID: dhcpServers,
|
||||
firewallConfigurationsByNodeID: firewalls,
|
||||
switchConfigurationsByNodeID: switches
|
||||
)
|
||||
@@ -194,6 +218,14 @@ func observation(_ item: TopologyRuntimeICMPObservation) -> JSON {
|
||||
]
|
||||
}
|
||||
|
||||
func dhcpStatus(_ status: TopologyRuntimeDHCPClientStatus) -> JSON {
|
||||
[
|
||||
"state": status.state.rawValue, "errors": status.errorCount,
|
||||
"server": status.selectedServerIPAddress ?? NSNull(), "offered": status.offeredIPAddress ?? NSNull(),
|
||||
"succeeded": status.succeeded,
|
||||
]
|
||||
}
|
||||
|
||||
func run(_ scenario: Scenario) -> JSON {
|
||||
var engine = TopologyNetworkRuntimeEngine(seed: scenario.seed)
|
||||
if let percent = scenario.speedPercent { engine.setSimulationSpeed(percent: percent) }
|
||||
@@ -269,6 +301,8 @@ func run(_ scenario: Scenario) -> JSON {
|
||||
results.append(engine.peerTCPSocketID(socketID: uuid(step.socket!))?.uuidString ?? NSNull())
|
||||
case "cleanTcp":
|
||||
results.append(engine.cleanClosedTCPSocket(socketID: uuid(step.socket!)))
|
||||
case "runDhcp":
|
||||
results.append(engine.runDHCPClient(nodeID: uuid(step.node!)).map(dhcpStatus) ?? NSNull())
|
||||
case "closeSocket":
|
||||
engine.closeSocket(socketID: uuid(step.socket!))
|
||||
results.append(engine.state.socketsByID[uuid(step.socket!)] == nil)
|
||||
@@ -298,6 +332,22 @@ func run(_ scenario: Scenario) -> JSON {
|
||||
"arp": arp,
|
||||
"switchTables": switchTables,
|
||||
"icmpObservations": observations,
|
||||
"devices": Dictionary(uniqueKeysWithValues: state.topologySnapshot.deviceConfigurations.map { node, device in
|
||||
(node.uuidString, [device.ipAddress, device.subnetMask, device.defaultGateway, device.dnsServer])
|
||||
}),
|
||||
"dhcp": [
|
||||
"clients": Dictionary(uniqueKeysWithValues: state.dhcpClientStatusesByNodeID.map { ($0.key.uuidString, dhcpStatus($0.value)) }),
|
||||
"leases": state.dhcpLeasesByIPAddress.sorted { $0.key < $1.key }.map { key, lease -> [Any] in
|
||||
[key, lease.clientMACAddress, lease.ipAddress, lease.expiresAtMilliseconds.map { $0 as Any } ?? NSNull()]
|
||||
},
|
||||
"offers": state.dhcpOffersByIPAddress.sorted { $0.key < $1.key }.map { key, offer -> [Any] in
|
||||
[key, offer.clientMACAddress, offer.expiresAtMilliseconds]
|
||||
},
|
||||
"blacklist": Dictionary(uniqueKeysWithValues: state.dhcpBlacklistByServerNodeID.map { node, entries in
|
||||
(node.uuidString, entries.sorted { $0.key < $1.key }.map { [$0.key, $0.value] as [Any] })
|
||||
}),
|
||||
"servers": Dictionary(uniqueKeysWithValues: state.dhcpServerSocketIDsByNodeID.map { ($0.key.uuidString, $0.value.uuidString) }),
|
||||
] as JSON,
|
||||
"sockets": state.socketsByID.values.sorted { $0.id.uuidString < $1.id.uuidString }.map { socket -> JSON in
|
||||
[
|
||||
"id": socket.id.uuidString, "node": socket.nodeID.uuidString, "protocol": socket.protocolKind.rawValue,
|
||||
|
||||
Reference in New Issue
Block a user