From 184f4e6ff611093e2d0190a01863fa8ed13f0e37 Mon Sep 17 00:00:00 2001 From: Kilobyte22 Date: Thu, 24 Jun 2021 19:23:05 +0200 Subject: [PATCH] Implement VLAN tagging and management interface for the managed switch. Also add a small util for configuring the switch --- .../tileentity/NetworkSwitchTileEntity.java | 341 ++++++++++++++++-- src/main/scripts/bin/swconfig.lua | 58 +++ 2 files changed, 372 insertions(+), 27 deletions(-) create mode 100644 src/main/scripts/bin/swconfig.lua diff --git a/src/main/java/li/cil/oc2/common/tileentity/NetworkSwitchTileEntity.java b/src/main/java/li/cil/oc2/common/tileentity/NetworkSwitchTileEntity.java index a5dd1a7a..2a2d08de 100644 --- a/src/main/java/li/cil/oc2/common/tileentity/NetworkSwitchTileEntity.java +++ b/src/main/java/li/cil/oc2/common/tileentity/NetworkSwitchTileEntity.java @@ -3,67 +3,118 @@ import java.util.*; import java.util.stream.Collectors; +import com.google.gson.internal.LinkedTreeMap; +import com.mojang.datafixers.util.Pair; import li.cil.oc2.api.bus.device.object.Callback; import li.cil.oc2.api.bus.device.object.DocumentedDevice; import li.cil.oc2.api.bus.device.object.NamedDevice; -import li.cil.oc2.api.bus.device.object.Parameter; import li.cil.oc2.api.capabilities.NetworkInterface; import li.cil.oc2.common.Constants; +import net.minecraft.block.BlockState; +import net.minecraft.nbt.*; import net.minecraft.tileentity.ITickableTileEntity; -import net.minecraft.util.Direction; - -import javax.annotation.Nullable; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; public final class NetworkSwitchTileEntity extends NetworkHubTileEntity implements ITickableTileEntity, NamedDevice, DocumentedDevice { private final String GET_HOST_TABLE = "getHostTable"; + private final String GET_PORT_CONFIG = "getPortConfig"; + private final String SET_PORT_CONFIG = "setPortConfig"; private final long HOST_TTL = 20 * 60 * 2; private final Map hostTable = new HashMap<>(); + private final PortSettings[] portSettings = new PortSettings[Constants.BLOCK_FACE_COUNT]; private int tickCount = 0; public NetworkSwitchTileEntity() { super(TileEntities.NETWORK_SWITCH_TILE_ENTITY.get()); - hostTable.put(0L, new HostEntry(0, Long.MAX_VALUE)); + for (int i = 0; i < portSettings.length; i++) { + portSettings[i] = new PortSettings(); + } } @Override - public void writeEthernetFrame(final NetworkInterface source, final byte[] frame, final int timeToLive) { - if (areAdjacentInterfacesDirty) { - hostTable.clear(); - } + public void writeEthernetFrame(final NetworkInterface source, byte[] frame, final int timeToLive) { validateAdjacentInterfaces(); long tickTime = getLevel().getGameTime(); long destMac = macToLong(frame, 0); long srcMac = macToLong(frame, 6); - Optional side = sideReverseLookup(source); - if (!side.isPresent()) { + short vlan = getVLAN(frame); + Optional optSide = sideReverseLookup(source); + if (!optSide.isPresent()) { return; } + int side = optSide.get(); if (hostTable.size() <= 256) { - hostTable.put(srcMac, new HostEntry(side.get(), tickTime)); + hostTable.put(srcMac, new HostEntry(side, tickTime)); } + PortSettings ingressSettings = portSettings[side]; + SwitchLog log = new SwitchLog(vlan, side, srcMac, destMac); + if (vlan == 0) { + // Untagged packet + Pair pair = removeVLANTag(frame); // Remove tag in case there is a 0-tag + frame = pair.getSecond(); + if (ingressSettings.untagged != 0) { + frame = addVLANTag(frame, ingressSettings.untagged); + vlan = ingressSettings.untagged; + } + } else { + if (!(ingressSettings.trunkAll || ingressSettings.tagged.contains(vlan))) { + // drop packet with disallowed vlan + log.drop("Tag not allowed for ingress"); + return; + } + } + HostEntry host = hostTable.get(destMac); if (host != null) { - if (host.iface == side.get()) { - // if packet is to same port, drop + if (host.iface == side && !ingressSettings.hairpin) { + // if packet is to same port and hairpin is disabled, drop + log.drop("hairpin disabled"); return; } - NetworkInterface iface = adjacentInterfaces[host.iface]; - if (iface != null) { - iface.writeEthernetFrame(this, frame, timeToLive - TTL_COST); - } - host.timestamp = tickTime; + writeToSide(frame, host.iface, vlan, log, timeToLive); } else { - super.writeEthernetFrame(source, frame, timeToLive); + log.flood(); + for (int i = 0; i < Constants.BLOCK_FACE_COUNT; i++) { + if (i != side) { + writeToSide(frame, i, vlan, log, timeToLive); + } + } + } + } + + private void writeToSide(byte[] frame, int side, short vlan, SwitchLog log, int timeToLive) { + log.egressPort(side); + NetworkInterface iface = adjacentInterfaces[side]; + if (iface != null) { + PortSettings egressSettings = portSettings[side]; + if (egressSettings.untagged != 0 && vlan == 0) { + log.drop("inner tag untagged"); + return; + } + + if (egressSettings.untagged == vlan) { + Pair pair = removeVLANTag(frame); + frame = pair.getSecond(); + log.egressVlan = 0; + } else if (!(egressSettings.trunkAll || egressSettings.tagged.contains(vlan))) { + // Drop packets with wrong tag + log.drop("Tag not allowed for egress"); + return; + } else { + log.egressVlan = vlan; + } + log.emit(); + iface.writeEthernetFrame(this, frame, timeToLive - TTL_COST); } } private long macToLong(final byte[] mac, int offset) { long ret = 0; for (int i = 0; i < 6; i++) { - ret |= (mac[i + offset] << i); + ret |= ((((long) mac[i + offset]) & 0xff) << (i * 8)); } return ret; } @@ -91,6 +142,59 @@ public Collection getDeviceTypeNames() { return singletonList("switch"); } + @Override + public CompoundNBT save(final CompoundNBT tag) { + super.save(tag); + + ListNBT hosts = new ListNBT(); + for (Map.Entry host : hostTable.entrySet()) { + CompoundNBT thisHost = new CompoundNBT(); + thisHost.put("mac", LongNBT.valueOf(host.getKey())); + thisHost.put("side", IntNBT.valueOf(host.getValue().iface)); + thisHost.put("timestamp", LongNBT.valueOf(host.getValue().timestamp)); + hosts.add(thisHost); + } + tag.put("hosts", hosts); + + ListNBT ports = new ListNBT(); + for (PortSettings myPort : portSettings) { + CompoundNBT port = new CompoundNBT(); + myPort.save(port); + ports.add(port); + } + tag.put("ports", ports); + + return tag; + } + + @Override + public void load(final BlockState blockState, final CompoundNBT tag) { + super.load(blockState, tag); + + INBT hosts = tag.get("hosts"); + if (hosts != null) { + for (INBT host_ : ((ListNBT) hosts)) { + CompoundNBT host = (CompoundNBT) host_; + hostTable.put( + host.getLong("mac"), + new HostEntry( + tag.getInt("side"), + tag.getLong("timestamp") + ) + ); + } + } + + INBT ports = tag.get("ports"); + if (ports != null) { + int i = 0; + for (INBT port : ((ListNBT) ports)) { + portSettings[i++] = PortSettings.load((CompoundNBT) port); + } + } + + } + @Callback(name = GET_HOST_TABLE) public List getHostTable() { long now = getLevel().getGameTime(); @@ -101,6 +205,19 @@ public List getHostTable() { .collect(Collectors.toList()); } + @Callback(name = GET_PORT_CONFIG, synchronize = false) + public PortSettings[] getPortSettings() { + return portSettings; + } + + @Callback(name = SET_PORT_CONFIG) + public void setPortSettings(List settings) { + int max = Math.min(portSettings.length, settings.size()); + for (int i = 0; i < max; i++) { + portSettings[i].untagged = ((Double) settings.get(i).get("untagged")).shortValue(); + } + } + private Optional sideReverseLookup(NetworkInterface iface) { for (int i = 0; i < Constants.BLOCK_FACE_COUNT; i++) { if (iface == adjacentInterfaces[i]) { @@ -110,18 +227,59 @@ private Optional sideReverseLookup(NetworkInterface iface) { return Optional.empty(); } - private String macLongToString(long mac) { + private static String macLongToString(long mac) { StringBuilder ret = new StringBuilder(); for (int i = 0; i < 6; i++) { if (i != 0) { ret.append(":"); } - ret.append(String.format("%02x", (mac >> i) & 0xff)); + ret.append(String.format("%02x", (mac >> (i * 8)) & 0xff)); } return ret.toString(); } - private class HostEntry { + private byte[] addVLANTag(byte[] packet, short tag) { + if (tag != 0) { + byte[] ret = new byte[packet.length + 4]; + copyBytes(packet, ret, 0, 0, 12); // Copy Ethernet Header + copyBytes(packet, ret, 12, 16, packet.length - 12); + ret[12] = (byte) 0x81; + ret[13] = (byte) 0x00; + ret[14] = (byte) ((tag >> 8) & 0x0f); + ret[15] = (byte) (tag & 0xff); + return ret; + } else { + return packet; + } + } + + private short getVLAN(byte[] packet) { + if (packet[12] == ((byte) 0x81) && packet[13] == 0x00) { + return (short) (packet[15] | ((((short) packet[14]) & 0x0f) << 8)); + } else { + return (short) 0; + } + } + + private Pair removeVLANTag(byte[] packet) { + if (packet[12] == ((byte) 0x81) && packet[13] == 0x00) { + byte[] ret = new byte[packet.length - 4]; + copyBytes(packet, ret, 0, 0, 12); // Copy Ethernet Header + copyBytes(packet, ret, 16, 12, packet.length - 16); // Copy payload + short tag = (short) (packet[15] | ((((short) packet[14]) & 0x0f) << 8)); // Extract vlan tag + return new Pair<>(tag, ret); + } else { + return new Pair<>((short) 0, packet); + } + } + + private void copyBytes(byte[] input, byte[] output, int inputOffset, int outputOffset, int length) { + for (int i = 0; i < length; i++) { + output[outputOffset + i] = input[inputOffset + i]; + } + } + + private static class HostEntry { public int iface; public long timestamp; public HostEntry(int iface, long timestamp) { @@ -130,15 +288,144 @@ public HostEntry(int iface, long timestamp) { } } - public class LuaHostEntry { + public static class LuaHostEntry { public String mac; public long age; - public int iface; + public int side; public LuaHostEntry(String mac, long age, int iface) { this.mac = mac; this.age = age; - this.iface = iface; + this.side = iface; + } + } + + private static class PortSettings { + /** + * The VLAN that is both PVID and untagged vlan. It will be removed on egress and added on ingress. If set to 0 + * this port is put on the global untagged vlan. The global untagged vlan can ever only be used as an untagged vlan + */ + public short untagged; + /** + * A list of tagged vlans that will be accepted on both ingress and egress. 0 (the global untagged vlan) is not a valid + * value + */ + public List tagged; + /** + * If enabled, packets entering on this port may also leave via this port again + */ + public boolean hairpin; + /** + * If this is set, tagged will be ignored. Instead all tagged vlans will be accepted. untagged will still be honored + */ + public boolean trunkAll; + + public PortSettings(final short untagged, final List tagged, final boolean hairpin, final boolean trunkAll) { + this.untagged = untagged; + this.tagged = tagged; + this.hairpin = hairpin; + this.trunkAll = trunkAll; + } + + /** + * Default configuration of an unmanaged switch, which just forwards all tagged vlans as well as the untagged vlan + * straight through + */ + public PortSettings() { + this((short) 0, emptyList(), false, true); + } + + public void save(final CompoundNBT tag) { + tag.put("untagged", ShortNBT.valueOf(untagged)); + tag.put("tagged", new IntArrayNBT(tagged.stream().map(s -> (int) s).collect(Collectors.toList()))); + tag.put("hairpin", ByteNBT.valueOf(hairpin)); + tag.put("trunkAll", ByteNBT.valueOf(trunkAll)); + } + + public static PortSettings load(final CompoundNBT tag) { + short untagged = tag.getShort("untagged"); + List tagged = Arrays.stream(tag.getIntArray("tagged")) + .mapToObj(i -> (short) i) + .collect(Collectors.toList()); + boolean hairpin = tag.getBoolean("hairpin"); + boolean trunkAll = tag.getBoolean("trunkAll"); + + return new PortSettings(untagged, tagged, hairpin, trunkAll); + } + } + + private static class SwitchLog { + private static final boolean ENABLED = true; + private short ingressVlan = 0; + private short egressVlan = 0; + private int ingressSide = 0; + private final long srcMac; + private final long destMac; + private Integer egressSide = null; + + public SwitchLog(short ingressVlan, int ingressSide, long srcMac, long destMac) { + this.ingressVlan = ingressVlan; + this.ingressSide = ingressSide; + this.srcMac = srcMac; + this.destMac = destMac; + } + + public void egressPort(int side) { + egressSide = side; + } + + public void drop(String reason) { + if (!ENABLED) return; + String inMac = NetworkSwitchTileEntity.macLongToString(srcMac); + String outMac = NetworkSwitchTileEntity.macLongToString(destMac); + if (egressSide == null) { + System.out.printf( + "Switch Packet %s (Port %s, VLAN %s) -> %s drop (%s)\n", + inMac, + ingressSide, + ingressVlan, + outMac, + reason + ); + } else { + System.out.printf( + "Switch Packet %s (Port %s, VLAN %s) -> %s (Port %s) drop (%s)\n", + inMac, + ingressSide, + ingressVlan, + outMac, + egressSide, + reason + ); + } + } + + public void emit() { + if (!ENABLED) return; + String inMac = NetworkSwitchTileEntity.macLongToString(srcMac); + String outMac = NetworkSwitchTileEntity.macLongToString(destMac); + System.out.printf( + "Switch Packet %s (Port %s, VLAN %s) -> %s (Port %s, VLAN %s)\n", + inMac, + ingressSide, + ingressVlan, + outMac, + egressSide, + egressVlan + ); + } + + public void flood() { + if (!ENABLED) return; + String inMac = NetworkSwitchTileEntity.macLongToString(srcMac); + String outMac = NetworkSwitchTileEntity.macLongToString(destMac); + System.out.printf( + "Switch Packet %s (Port %s, VLAN %s) -> %s flood\n", + inMac, + ingressSide, + ingressVlan, + outMac + ); } } } diff --git a/src/main/scripts/bin/swconfig.lua b/src/main/scripts/bin/swconfig.lua new file mode 100644 index 00000000..031c8ee1 --- /dev/null +++ b/src/main/scripts/bin/swconfig.lua @@ -0,0 +1,58 @@ +#!/usr/bin/lua + +function usage() + print("Usage:") + print(" swconfig show_hosts") + print(" swconfig show_ports") + print(" swconfig set_port untagged ") + print(" swconfig set_port trunk_all (on|off)") +end + +if not arg[1] then + usage() + return +end + +local cjson = require("cjson").new() + +local devbus = require('devices') +local switch = devbus:find("switch") + +if not switch then + print("No switch found") + return +end + +if arg[1] == "show_hosts" then + host_table = switch:getHostTable() + + for _, v in ipairs(host_table) do + print(v.mac .. " : " .. v.side .. ", Age: " .. v.age) + end +elseif arg[1] == "show_ports" then + for i, port in ipairs(switch:getPortConfig()) do + print("Port #" .. (i - 1)) + print(" Untagged VLAN: " .. port.untagged) + print(" Tagged: " .. table.concat(port.tagged, ", ")) + print(" Hairpin: " .. (port.hairpin and "on" or "off")) + print(" Trunk All: " .. (port.hairpin and "on" or "off")) + end +elseif arg[1] == "set_port" then + if #arg < 4 then + usage() + return + end + local config = switch:getPortConfig() + local port = config[tonumber(arg[2]) + 1] + if not port then + print("Invalid Port Number") + return + end + if arg[3] == "untagged" then + port.untagged = tonumber(arg[4]) + end + switch:setPortConfig(config) +else + usage() + return +end \ No newline at end of file