// A core scenario's network (packages/core/test/scenarios/*.scenario.json: // nodes, links (wireless or not), devices, switches' and hosts' SSIDs, // dhcpServers, dhcpClients, …) built in Java FILIUS the // way the core reads it: a port's MAC is its `mac`, else the core's // stableGeneratedMacAddress. Started as the action mode starts it (every // system software's starten()); afterwards every frame each network card saw // (FILIUS' own packet log, the Lauscher) and each host's addresses. Shared by // DhcpRun and TcpRun. import filius.gui.GUIContainer; import filius.hardware.Kabel; import filius.hardware.NetzwerkInterface; import filius.hardware.Port; import filius.hardware.knoten.Gateway; import filius.hardware.knoten.InternetKnoten; import filius.hardware.knoten.Knoten; import filius.hardware.knoten.LokalerKnoten; import filius.hardware.knoten.Notebook; import filius.hardware.knoten.Rechner; import filius.hardware.knoten.Switch; import filius.hardware.knoten.Vermittlungsrechner; import filius.rahmenprogramm.nachrichten.Lauscher; import filius.software.dhcp.DHCPServer; import filius.software.dns.DNSServer; import filius.software.netzzugangsschicht.EthernetFrame; import filius.software.system.Betriebssystem; import filius.software.system.GatewayFirmware; import filius.software.system.InternetKnotenBetriebssystem; import filius.software.system.SwitchFirmware; import filius.software.system.VermittlungsrechnerBetriebssystem; import filius.software.transportschicht.Segment; import filius.software.transportschicht.TcpSegment; import filius.software.vermittlungsschicht.ArpPaket; import filius.software.vermittlungsschicht.IcmpPaket; import filius.software.vermittlungsschicht.IpPaket; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeMap; public class ScenarioNetwork { final Map nodes = new LinkedHashMap<>(); /** Each card's MAC address, under its node's id (the first card) or `id#index` (the others). */ final Map macByNode = new LinkedHashMap<>(); final List servers = new ArrayList<>(); long start; @SuppressWarnings("unchecked") static ScenarioNetwork read(String path) throws Exception { Map scenario = (Map) new JsonReader( new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8)).value(); ScenarioNetwork network = new ScenarioNetwork(); network.build(scenario); return network; } /** The scenario's seed. */ @SuppressWarnings("unchecked") static long seed(String path) throws Exception { Map scenario = (Map) new JsonReader( new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8)).value(); return ((Number) scenario.get("seed")).longValue(); } private void build(Map scenario) { Map ports = new HashMap<>(); Map nicsByPort = new HashMap<>(); for (Object each : list(scenario.get("nodes"))) { Map node = map(each); String id = (String) node.get("id"); String kind = (String) node.get("kind"); List nodePorts = list(node.get("ports")); Knoten knoten; switch (kind) { case "pc": knoten = new Rechner(); break; case "notebook": knoten = new Notebook(); break; case "networkSwitch": knoten = new Switch(); break; case "router": knoten = new Vermittlungsrechner(); ((InternetKnoten) knoten).setzeAnzahlAnschluesse(nodePorts.size()); break; case "gateway": // Its first card is the WAN port, its second the LAN port. knoten = new Gateway(); break; default: throw new IllegalArgumentException("node kind " + kind + " is not built by this tool"); } knoten.setName(id); if (knoten instanceof InternetKnoten) { List nics = ((InternetKnoten) knoten).getNetzwerkInterfaces(); for (int index = 0; index < nodePorts.size(); index++) { Map port = map(nodePorts.get(index)); NetzwerkInterface nic = nics.get(index); String mac = port.containsKey("mac") ? (String) port.get("mac") : stableMac((String) port.get("id")); nic.setMac(mac); ports.put((String) port.get("id"), nic.getPort()); nicsByPort.put((String) port.get("id"), nic); macByNode.put(index == 0 ? id : id + "#" + index, mac); } } else { LinkedList switchPorts = ((LokalerKnoten) knoten).getAnschluesse(); for (int index = 0; index < nodePorts.size(); index++) { ports.put((String) map(nodePorts.get(index)).get("id"), switchPorts.get(index)); } } nodes.put(id, knoten); } for (Map.Entry entry : map(scenario.getOrDefault("devices", new HashMap<>())).entrySet()) { Map device = map(entry.getValue()); NetzwerkInterface nic = ((InternetKnoten) nodes.get(entry.getKey())).getNetzwerkInterfaces().get(0); nic.setIp((String) device.get("ip")); nic.setSubnetzMaske((String) device.get("mask")); nic.setGateway((String) device.getOrDefault("gateway", "")); nic.setDns((String) device.getOrDefault("dns", "")); } // A router's other cards: `interfaces` names them by port. for (Object each : list(scenario.getOrDefault("interfaces", new ArrayList<>()))) { Map item = map(each); NetzwerkInterface nic = nicsByPort.get(item.get("port")); nic.setIp((String) item.get("ip")); nic.setSubnetzMaske((String) item.get("mask")); } for (Object each : list(scenario.get("links"))) { Map link = map(each); Kabel cable = new Kabel(); cable.setAnschluesse(new Port[] { ports.get(link.get("sourcePort")), ports.get(link.get("targetPort")) }); cable.setWireless(Boolean.TRUE.equals(link.get("wireless"))); } // A switch's SSID and learning time; a host's wireless card and the SSID // it joins. The SSIDs only the editor reads, to lay a wireless cable. for (Map.Entry entry : map(scenario.getOrDefault("switches", new HashMap<>())).entrySet()) { Map config = map(entry.getValue()); SwitchFirmware firmware = (SwitchFirmware) nodes.get(entry.getKey()).getSystemSoftware(); firmware.setSSID((String) config.get("ssid")); // How long the switch keeps a learned MAC address (Java's default 300 s). if (config.containsKey("retention")) firmware.setRetentionTime(((Number) config.get("retention")).longValue()); } for (Map.Entry entry : map(scenario.getOrDefault("wireless", new HashMap<>())).entrySet()) { Betriebssystem os = (Betriebssystem) system(nodes.get(entry.getKey())); os.nicWireless(true); os.setSsid((String) map(entry.getValue()).get("ssid")); } for (Map.Entry entry : map(scenario.getOrDefault("dhcpServers", new HashMap<>())).entrySet()) { Map config = map(entry.getValue()); DHCPServer server = system(nodes.get(entry.getKey())).getDHCPServer(); server.setUntergrenze((String) config.getOrDefault("lower", "0.0.0.0")); server.setObergrenze((String) config.getOrDefault("upper", "0.0.0.0")); server.setGatewayip((String) config.getOrDefault("gateway", "0.0.0.0")); server.setDnsserverip((String) config.getOrDefault("dns", "0.0.0.0")); server.setOwnSettings(Boolean.TRUE.equals(config.get("own"))); for (Object assignment : list(config.getOrDefault("staticAssignments", new ArrayList<>()))) { server.addStaticAssignment((String) list(assignment).get(0), (String) list(assignment).get(1)); } server.setAktiv(Boolean.TRUE.equals(config.get("active"))); servers.add(entry.getKey()); } // A DNS server: installed, its records written to /dns/hosts (TTL 3600), switched on. for (Map.Entry entry : map(scenario.getOrDefault("dnsServers", new HashMap<>())).entrySet()) { Map config = map(entry.getValue()); InternetKnotenBetriebssystem os = system(nodes.get(entry.getKey())); DNSServer dns = new DNSServer(); dns.setSystemSoftware(os); os.getInstallierteAnwendungen().put("filius.software.dns.DNSServer", dns); for (Object line : list(config.getOrDefault("records", new ArrayList<>()))) { String[] parts = ((String) line).split(" "); dns.hinzuRecord(parts[0], parts[1], parts[2]); } dns.setRecursiveResolutionEnabled(Boolean.TRUE.equals(config.get("recursive"))); dns.setAktiv(true); servers.add(entry.getKey()); } for (Object each : list(scenario.getOrDefault("dhcpClients", new ArrayList<>()))) { system(nodes.get((String) each)).setDHCPKonfiguration(true); } // A home router's port forwarding: rows of protocol, public port, LAN address, LAN port. for (Map.Entry entry : map(scenario.getOrDefault("portForwarding", new HashMap<>())).entrySet()) { LinkedList rows = new LinkedList<>(); for (Object row : list(entry.getValue())) rows.add(list(row).stream().map(String::valueOf).toArray(String[]::new)); ((GatewayFirmware) system(nodes.get(entry.getKey()))).setStaticNAT(rows); } for (Object each : list(scenario.getOrDefault("rip", new ArrayList<>()))) { ((VermittlungsrechnerBetriebssystem) system(nodes.get((String) each))).setRipEnabled(true); } } /** Starts the simulation; times in the log count from here. */ void start() throws InterruptedException { // Switches and DHCP servers first: FILIUS' client waits for every active // server to have started (waitUntilDhcpServerStarted, through the GUI's // node list, which this tool has not got). // FILIUS' window exists before a simulation starts; created lazily and // unsynchronized, the first clients to ask for it would race. GUIContainer.getGUIContainer(); start = System.currentTimeMillis(); for (Map.Entry entry : nodes.entrySet()) { if (entry.getValue() instanceof Switch || servers.contains(entry.getKey())) entry.getValue().getSystemSoftware().starten(); } Thread.sleep(200); for (Map.Entry entry : nodes.entrySet()) { if (!(entry.getValue() instanceof Switch || servers.contains(entry.getKey()))) entry.getValue().getSystemSoftware().starten(); } } InternetKnotenBetriebssystem system(String nodeId) { return (InternetKnotenBetriebssystem) nodes.get(nodeId).getSystemSoftware(); } /** Every frame each host's card saw, by node. */ void writeFrames(Json out) throws Exception { Field field = Lauscher.class.getDeclaredField("datenEinheiten"); field.setAccessible(true); @SuppressWarnings("unchecked") Map> log = (Map>) field.get(Lauscher.getLauscher()); out.key("frames").open('{'); for (Map.Entry entry : macByNode.entrySet()) { out.key(entry.getKey()).open('['); LinkedList frames = log.get(entry.getValue()); if (frames != null) { synchronized (frames) { for (Object[] stamped : frames) frame(out, ((Long) stamped[0]) - start, (EthernetFrame) stamped[1]); } } out.close(']'); } out.close('}'); writeRows(out, "rows", true); writeRows(out, "layeredRows", false); } /** * The packet capture's table of each card as FILIUS shows it * (Lauscher.getDaten): with addresses and ports (`rows`, the data exchange * window) or ports only (`layeredRows`, the layered view). Each row: number, * source, destination, protocol, layer, comment — its time (the wall clock) * left out. */ void writeRows(Json out, String key, boolean withAddresses) { out.key(key).open('{'); for (Map.Entry entry : macByNode.entrySet()) { out.key(entry.getKey()).open('['); for (Object[] row : Lauscher.getLauscher().getDaten(entry.getValue(), withAddresses, 1)) { out.open('['); for (int column = 0; column < row.length; column++) { if (column == 1) continue; out.string(row[column] == null ? null : String.valueOf(row[column])); } out.close(']'); } out.close(']'); } out.close('}'); } /** Each host's address, mask, gateway and DNS server. */ void writeDevices(Json out) { out.key("devices").open('{'); for (Map.Entry entry : nodes.entrySet()) { if (!(entry.getValue() instanceof InternetKnoten)) continue; NetzwerkInterface nic = ((InternetKnoten) entry.getValue()).getNetzwerkInterfaces().get(0); out.key(entry.getKey()).open('[').string(nic.getIp()).string(nic.getSubnetzMaske()).string(nic.getGateway()) .string(nic.getDns()).close(']'); } out.close('}'); } void stop() { for (Knoten knoten : nodes.values()) { try { knoten.getSystemSoftware().beenden(); } catch (Exception e) {} } } static void write(String path, Json out) throws Exception { try (Writer writer = new OutputStreamWriter(new FileOutputStream(path), StandardCharsets.UTF_8)) { writer.write(out.toString()); writer.write('\n'); } } private static void frame(Json out, long time, EthernetFrame frame) { out.open('{').key("time").number(time).key("source").string(frame.getQuellMacAdresse()).key("destination") .string(frame.getZielMacAdresse()); Object data = frame.getDaten(); if (data instanceof ArpPaket) { ArpPaket arp = (ArpPaket) data; out.key("arp").number(arp.getOperation()).key("senderIp").string(arp.getSenderIP()).key("targetIp") .string(arp.getTargetIP()); } else if (data instanceof IcmpPaket) { IcmpPaket icmp = (IcmpPaket) data; out.key("icmp").number(icmp.getIcmpType()).key("icmpCode").number(icmp.getIcmpCode()).key("sourceIp") .string(icmp.getSender()).key("destinationIp").string(icmp.getEmpfaenger()).key("sequence") .number(icmp.getSeqNr()); } else if (data instanceof IpPaket) { IpPaket ip = (IpPaket) data; Segment segment = ip.getSegment(); out.key("sourceIp").string(ip.getSender()).key("destinationIp").string(ip.getEmpfaenger()).key("protocol") .number(ip.getProtocol()).key("sourcePort").number(segment.getQuellPort()).key("destinationPort") .number(segment.getZielPort()).key("payload").string(segment.getDaten()); if (segment instanceof TcpSegment) { TcpSegment tcp = (TcpSegment) segment; String flags = (tcp.isSyn() ? "S" : "") + (tcp.isAck() ? "A" : "") + (tcp.isFin() ? "F" : "") + (tcp.isPush() ? "P" : "") + (tcp.isRst() ? "R" : ""); out.key("tcpFlags").string(flags).key("sequence").number(tcp.getSeqNummer()).key("acknowledgement") .number(tcp.getAckNummer()); } } out.close('}'); } private static InternetKnotenBetriebssystem system(Knoten knoten) { return (InternetKnotenBetriebssystem) knoten.getSystemSoftware(); } /** The core's stableGeneratedMacAddress (packages/core/src/model/node.ts). */ static String stableMac(String portId) { String compact = portId.toUpperCase().replace("-", ""); String suffix = compact.substring(compact.length() - 10); StringBuilder mac = new StringBuilder("02"); for (int offset = 0; offset < suffix.length(); offset += 2) mac.append(':').append(suffix, offset, offset + 2); return mac.toString(); } @SuppressWarnings("unchecked") private static Map map(Object value) { return (Map) value; } @SuppressWarnings("unchecked") private static List list(Object value) { return (List) value; } /** Just enough JSON for a scenario: objects, arrays, strings, numbers, booleans, null. */ static final class JsonReader { private final String text; private int position; JsonReader(String text) { this.text = text; } Object value() { skip(); char c = text.charAt(position); if (c == '{') { Map result = new TreeMap<>(); position++; skip(); if (text.charAt(position) == '}') { position++; return result; } for (;;) { skip(); String key = (String) value(); skip(); expect(':'); result.put(key, value()); skip(); if (text.charAt(position++) == '}') return result; } } if (c == '[') { List result = new ArrayList<>(); position++; skip(); if (text.charAt(position) == ']') { position++; return result; } for (;;) { result.add(value()); skip(); if (text.charAt(position++) == ']') return result; } } if (c == '"') { StringBuilder result = new StringBuilder(); position++; for (char each = text.charAt(position++); each != '"'; each = text.charAt(position++)) { if (each == '\\') { char escaped = text.charAt(position++); switch (escaped) { case 'n': result.append('\n'); break; case 't': result.append('\t'); break; case 'u': result.append((char) Integer.parseInt(text.substring(position, position + 4), 16)); position += 4; break; default: result.append(escaped); } } else { result.append(each); } } return result.toString(); } if (text.startsWith("true", position)) { position += 4; return Boolean.TRUE; } if (text.startsWith("false", position)) { position += 5; return Boolean.FALSE; } if (text.startsWith("null", position)) { position += 4; return null; } int begin = position; while (position < text.length() && "+-.eE0123456789".indexOf(text.charAt(position)) >= 0) position++; return Double.parseDouble(text.substring(begin, position)); } private void skip() { while (position < text.length() && Character.isWhitespace(text.charAt(position))) position++; } private void expect(char c) { if (text.charAt(position++) != c) throw new IllegalArgumentException("expected " + c + " at " + (position - 1)); } } /** A JSON writer with commas and indentation. */ static final class Json { private final StringBuilder text = new StringBuilder(); private final LinkedList first = new LinkedList<>(); private boolean afterKey; private void separate() { if (afterKey) { afterKey = false; return; } if (first.isEmpty()) return; if (!first.peek()) text.append(','); first.pop(); first.push(false); text.append('\n'); for (int i = 0; i < first.size(); i++) text.append(" "); } Json open(char bracket) { separate(); text.append(bracket); first.push(true); return this; } Json close(char bracket) { boolean empty = first.pop(); if (!empty) { text.append('\n'); for (int i = 0; i < first.size(); i++) text.append(" "); } text.append(bracket); return this; } Json key(String key) { separate(); quote(key); text.append(": "); afterKey = true; return this; } Json string(String value) { separate(); if (value == null) text.append("null"); else quote(value); return this; } Json number(long value) { separate(); text.append(value); return this; } private void quote(String value) { text.append('"'); for (char c : value.toCharArray()) { if (c == '"' || c == '\\') text.append('\\').append(c); else if (c == '\n') text.append("\\n"); else if (c < 0x20) text.append(String.format("\\u%04x", (int) c)); else text.append(c); } text.append('"'); } @Override public String toString() { return text.toString(); } } }