diff --git a/src/main/java/li/cil/oc2/common/block/Blocks.java b/src/main/java/li/cil/oc2/common/block/Blocks.java index 0f9c4e87..9330a681 100644 --- a/src/main/java/li/cil/oc2/common/block/Blocks.java +++ b/src/main/java/li/cil/oc2/common/block/Blocks.java @@ -21,6 +21,7 @@ public final class Blocks { public static final RegistryObject KEYBOARD = BLOCKS.register("keyboard", KeyboardBlock::new); public static final RegistryObject NETWORK_CONNECTOR = BLOCKS.register("network_connector", NetworkConnectorBlock::new); public static final RegistryObject NETWORK_HUB = BLOCKS.register("network_hub", NetworkHubBlock::new); + public static final RegistryObject NETWORK_SWITCH = BLOCKS.register("network_switch", NetworkSwitchBlock::new); public static final RegistryObject PROJECTOR = BLOCKS.register("projector", ProjectorBlock::new); public static final RegistryObject REDSTONE_INTERFACE = BLOCKS.register("redstone_interface", RedstoneInterfaceBlock::new); diff --git a/src/main/java/li/cil/oc2/common/block/NetworkSwitchBlock.java b/src/main/java/li/cil/oc2/common/block/NetworkSwitchBlock.java new file mode 100644 index 00000000..03b62cc2 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/block/NetworkSwitchBlock.java @@ -0,0 +1,61 @@ +package li.cil.oc2.common.block; + +import li.cil.oc2.common.blockentity.BlockEntities; +import li.cil.oc2.common.blockentity.NetworkHubBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.material.Material; + +import javax.annotation.Nullable; + +public final class NetworkSwitchBlock extends HorizontalDirectionalBlock implements EntityBlock { + public NetworkSwitchBlock() { + super(Properties + .of(Material.METAL) + .sound(SoundType.METAL) + .strength(1.5f, 6.0f)); + registerDefaultState(getStateDefinition().any().setValue(FACING, Direction.NORTH)); + } + + /////////////////////////////////////////////////////////////////// + + @Override + public BlockState getStateForPlacement(final BlockPlaceContext context) { + return super.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @SuppressWarnings("deprecation") + @Override + public void neighborChanged(final BlockState state, final Level level, final BlockPos pos, final Block changedBlock, final BlockPos changedBlockPos, final boolean isMoving) { + final BlockEntity blockEntity = level.getBlockEntity(pos); + if (blockEntity instanceof final NetworkHubBlockEntity networkHub) { + networkHub.handleNeighborChanged(); + } + } + + /////////////////////////////////////////////////////////////////// + // EntityBlock + + @Nullable + @Override + public BlockEntity newBlockEntity(final BlockPos pos, final BlockState state) { + return BlockEntities.NETWORK_SWITCH.get().create(pos, state); + } + + /////////////////////////////////////////////////////////////////// + + @Override + protected void createBlockStateDefinition(final StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(FACING); + } +} diff --git a/src/main/java/li/cil/oc2/common/blockentity/BlockEntities.java b/src/main/java/li/cil/oc2/common/blockentity/BlockEntities.java index 1c885086..9b0eb2cc 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/BlockEntities.java +++ b/src/main/java/li/cil/oc2/common/blockentity/BlockEntities.java @@ -24,6 +24,7 @@ public final class BlockEntities { public static final RegistryObject> KEYBOARD = register(Blocks.KEYBOARD, KeyboardBlockEntity::new); public static final RegistryObject> NETWORK_CONNECTOR = register(Blocks.NETWORK_CONNECTOR, NetworkConnectorBlockEntity::new); public static final RegistryObject> NETWORK_HUB = register(Blocks.NETWORK_HUB, NetworkHubBlockEntity::new); + public static final RegistryObject> NETWORK_SWITCH = register(Blocks.NETWORK_SWITCH, NetworkSwitchBlockEntity::new); public static final RegistryObject> PROJECTOR = register(Blocks.PROJECTOR, ProjectorBlockEntity::new); public static final RegistryObject> REDSTONE_INTERFACE = register(Blocks.REDSTONE_INTERFACE, RedstoneInterfaceBlockEntity::new); diff --git a/src/main/java/li/cil/oc2/common/blockentity/NetworkSwitchBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/NetworkSwitchBlockEntity.java new file mode 100644 index 00000000..d73dd382 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/blockentity/NetworkSwitchBlockEntity.java @@ -0,0 +1,498 @@ +package li.cil.oc2.common.blockentity; + +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.capabilities.NetworkInterface; +import li.cil.oc2.common.Constants; +import li.cil.oc2.common.block.NetworkSwitchBlock; +import li.cil.oc2.common.blockentity.BlockEntities; +import li.cil.oc2.common.blockentity.NetworkHubBlockEntity; +import li.cil.oc2.common.capabilities.Capabilities; +import li.cil.oc2.common.util.LazyOptionalUtils; +import li.cil.oc2.common.util.LevelUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.util.LazyOptional; + + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +public final class NetworkSwitchBlockEntity extends ModBlockEntity implements NamedDevice, DocumentedDevice, NetworkInterface, TickableBlockEntity { + private final String GET_LINK_STATE = "getLinkState"; + 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 int TTL_COST = 1; + private final Map hostTable = new HashMap<>(); + private final PortSettings[] portSettings = new PortSettings[Constants.BLOCK_FACE_COUNT]; + private int tickCount = 0; + private final NetworkInterface[] adjacentBlockInterfaces = new NetworkInterface[Constants.BLOCK_FACE_COUNT]; + private boolean haveAdjacentBlocksChanged = true; + + public NetworkSwitchBlockEntity(final BlockPos pos, final BlockState state) { + super(BlockEntities.NETWORK_SWITCH.get(), pos, state); + for (int i = 0; i < portSettings.length; i++) { + portSettings[i] = new PortSettings(); + } + } + + public void writeEthernetFrame(final NetworkInterface source, byte[] frame, final int timeToLive) { + validateAdjacentBlocks(); + long tickTime = getLevel().getGameTime(); + long destMac = macToLong(frame, 0); + long srcMac = macToLong(frame, 6); + 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, 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 && !ingressSettings.hairpin) { + // if packet is to same port and hairpin is disabled, drop + log.drop("hairpin disabled"); + return; + } + writeToSide(frame, host.iface, vlan, log, timeToLive); + } else { + log.flood(); + for (int i = 0; i < Constants.BLOCK_FACE_COUNT; i++) { + if (i != side) { + writeToSide(frame, i, vlan, log, timeToLive); + } + } + } + } + + @Override + public byte[] readEthernetFrame() { + return null; + } + + private void writeToSide(byte[] frame, int side, short vlan, SwitchLog log, int timeToLive) { + log.egressPort(side); + NetworkInterface iface = adjacentBlockInterfaces[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 |= ((((long) mac[i + offset]) & 0xff) << (i * 8)); + } + return ret; + } + + @Override + public void clientTick() + { + return; + } + + @Override + public void serverTick() { + if (level == null) { + return; + } + if (tickCount++ % 20 == 0) { + long threshold = getLevel().getGameTime() - HOST_TTL; + if (threshold < 0) { + return; + } + hostTable.entrySet().removeIf(e -> e.getValue().timestamp < threshold); + } + } + + @Override + public void getDeviceDocumentation(final DeviceVisitor visitor) { + visitor.visitCallback(GET_HOST_TABLE) + .description("Returns the MAC address table of the switch") + .returnValueDescription("The MAC table. For each host the mac address, the age (in ticks) and the face is returned"); + } + + @Override + public Collection getDeviceTypeNames() { + return singletonList("switch"); + } + + @Override + public void saveAdditional(final CompoundTag tag) { + super.saveAdditional(tag); + + ListTag hosts = new ListTag(); + for (Map.Entry host : hostTable.entrySet()) { + CompoundTag thisHost = new CompoundTag(); + thisHost.put("mac", LongTag.valueOf(host.getKey())); + thisHost.put("side", IntTag.valueOf(host.getValue().iface)); + thisHost.put("timestamp", LongTag.valueOf(host.getValue().timestamp)); + hosts.add(thisHost); + } + tag.put("hosts", hosts); + + ListTag ports = new ListTag(); + for (PortSettings myPort : portSettings) { + CompoundTag port = new CompoundTag(); + myPort.save(port); + ports.add(port); + } + tag.put("ports", ports); + } + + @Override + public void load(final CompoundTag tag) { + super.load(tag); + + Tag hosts = tag.get("hosts"); + if (hosts != null) { + for (Tag host_ : ((ListTag) hosts)) { + CompoundTag host = (CompoundTag) host_; + hostTable.put( + host.getLong("mac"), + new HostEntry( + tag.getInt("side"), + tag.getLong("timestamp") + ) + ); + } + } + + Tag ports = tag.get("ports"); + if (ports != null) { + int i = 0; + for (Tag port : ((ListTag) ports)) { + portSettings[i++] = PortSettings.load((CompoundTag) port); + } + } + + } + + @Callback(name = GET_HOST_TABLE) + public List getHostTable() { + long now = getLevel().getGameTime(); + return hostTable + .entrySet() + .stream() + .map(e -> new LuaHostEntry(macLongToString(e.getKey()), now - e.getValue().timestamp, e.getValue().iface)) + .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(); + } + } + + @Callback(name = GET_LINK_STATE) + public boolean[] getLinkState() { + validateAdjacentBlocks(); + boolean[] sides = new boolean[Constants.BLOCK_FACE_COUNT]; + for (int i = 0; i < Constants.BLOCK_FACE_COUNT; i++) { + sides[i] = adjacentBlockInterfaces[i] != null; + } + return sides; + } + + private Optional sideReverseLookup(NetworkInterface iface) { + for (int i = 0; i < Constants.BLOCK_FACE_COUNT; i++) { + if (iface == adjacentBlockInterfaces[i]) { + return Optional.of(i); + } + } + return Optional.empty(); + } + + 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 * 8)) & 0xff)); + } + return ret.toString(); + } + + 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 void validateAdjacentBlocks() { + if (isRemoved() || !haveAdjacentBlocksChanged) { + return; + } + + for (final Direction side : Constants.DIRECTIONS) { + adjacentBlockInterfaces[side.get3DDataValue()] = null; + } + + haveAdjacentBlocksChanged = false; + + if (level == null || level.isClientSide()) { + return; + } + + final BlockPos pos = getBlockPos(); + for (final Direction side : Constants.DIRECTIONS) { + final BlockEntity neighborBlockEntity = LevelUtils.getBlockEntityIfChunkExists(level, pos.relative(side)); + if (neighborBlockEntity != null) { + final LazyOptional optional = neighborBlockEntity.getCapability(Capabilities.networkInterface(), side.getOpposite()); + optional.ifPresent(adjacentInterface -> { + adjacentBlockInterfaces[side.get3DDataValue()] = adjacentInterface; + LazyOptionalUtils.addWeakListener(optional, this, (hub, unused) -> hub.handleNeighborChanged()); + }); + } + } + } + + private void handleNeighborChanged() { + haveAdjacentBlocksChanged = true; + } + + private static class HostEntry { + public int iface; + public long timestamp; + public HostEntry(int iface, long timestamp) { + this.iface = iface; + this.timestamp = timestamp; + } + } + + public static class LuaHostEntry { + public String mac; + public long age; + public int side; + + public LuaHostEntry(String mac, long age, int iface) { + this.mac = mac; + this.age = age; + 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 CompoundTag tag) { + tag.put("untagged", ShortTag.valueOf(untagged)); + tag.put("tagged", new IntArrayTag(tagged.stream().map(s -> (int) s).collect(Collectors.toList()))); + tag.put("hairpin", ByteTag.valueOf(hairpin)); + tag.put("trunkAll", ByteTag.valueOf(trunkAll)); + } + + public static PortSettings load(final CompoundTag 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 = NetworkSwitchBlockEntity.macLongToString(srcMac); + String outMac = NetworkSwitchBlockEntity.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 = NetworkSwitchBlockEntity.macLongToString(srcMac); + String outMac = NetworkSwitchBlockEntity.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 = NetworkSwitchBlockEntity.macLongToString(srcMac); + String outMac = NetworkSwitchBlockEntity.macLongToString(destMac); + System.out.printf( + "Switch Packet %s (Port %s, VLAN %s) -> %s flood\n", + inMac, + ingressSide, + ingressVlan, + outMac + ); + } + } +} diff --git a/src/main/java/li/cil/oc2/common/item/Items.java b/src/main/java/li/cil/oc2/common/item/Items.java index 09a630d8..8cfdbd1e 100644 --- a/src/main/java/li/cil/oc2/common/item/Items.java +++ b/src/main/java/li/cil/oc2/common/item/Items.java @@ -31,6 +31,7 @@ public final class Items { public static final RegistryObject KEYBOARD = register(Blocks.KEYBOARD); public static final RegistryObject NETWORK_CONNECTOR = register(Blocks.NETWORK_CONNECTOR); public static final RegistryObject NETWORK_HUB = register(Blocks.NETWORK_HUB); + public static final RegistryObject NETWORK_SWITCH = register(Blocks.NETWORK_SWITCH); public static final RegistryObject PROJECTOR = register(Blocks.PROJECTOR); public static final RegistryObject REDSTONE_INTERFACE = register(Blocks.REDSTONE_INTERFACE); diff --git a/src/main/java/li/cil/oc2/data/ModBlockStateProvider.java b/src/main/java/li/cil/oc2/data/ModBlockStateProvider.java index 722e43ff..30d6964f 100644 --- a/src/main/java/li/cil/oc2/data/ModBlockStateProvider.java +++ b/src/main/java/li/cil/oc2/data/ModBlockStateProvider.java @@ -54,6 +54,7 @@ protected void registerStatesAndModels() { .end() .end(); horizontalBlock(Blocks.NETWORK_HUB, Items.NETWORK_HUB, NETWORK_HUB_MODEL); + horizontalBlock(Blocks.NETWORK_SWITCH, Items.NETWORK_SWITCH, NETWORK_HUB_MODEL); horizontalBlock(Blocks.PROJECTOR, Items.PROJECTOR, PROJECTOR_MODEL); horizontalBlock(Blocks.REDSTONE_INTERFACE, Items.REDSTONE_INTERFACE, REDSTONE_INTERFACE_MODEL); diff --git a/src/main/resources/assets/oc2/blockstates/network_switch.json b/src/main/resources/assets/oc2/blockstates/network_switch.json new file mode 100644 index 00000000..99bb9127 --- /dev/null +++ b/src/main/resources/assets/oc2/blockstates/network_switch.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "oc2:block/network_hub" + }, + "facing=south": { + "model": "oc2:block/network_hub", + "y": 180 + }, + "facing=west": { + "model": "oc2:block/network_hub", + "y": 270 + }, + "facing=east": { + "model": "oc2:block/network_hub", + "y": 90 + } + } +} diff --git a/src/main/scripts/bin/swconfig.lua b/src/main/scripts/bin/swconfig.lua new file mode 100644 index 00000000..6ef47192 --- /dev/null +++ b/src/main/scripts/bin/swconfig.lua @@ -0,0 +1,59 @@ +#!/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 + local link_state = switch:getLinkState() + for i, port in ipairs(switch:getPortConfig()) do + print("Port #" .. (i - 1) .. " " .. (link_state[i] and "UP" or "DOWN")) + print(" Untagged VLAN: " .. port.untagged) + print(" Tagged: " .. table.concat(port.tagged, ", ")) + print(" Hairpin: " .. (port.hairpin and "on" or "off")) + print(" Trunk All: " .. (port.trunk_all 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