diff --git a/src/main/java/li/cil/oc2/api/inet/DatagramSession.java b/src/main/java/li/cil/oc2/api/inet/DatagramSession.java new file mode 100644 index 000000000..35e8233fa --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/DatagramSession.java @@ -0,0 +1,4 @@ +package li.cil.oc2.api.inet; + +public interface DatagramSession extends Session { +} diff --git a/src/main/java/li/cil/oc2/api/inet/EchoSession.java b/src/main/java/li/cil/oc2/api/inet/EchoSession.java new file mode 100644 index 000000000..80ff5a7d9 --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/EchoSession.java @@ -0,0 +1,7 @@ +package li.cil.oc2.api.inet; + +public interface EchoSession extends Session { + int getSequenceNumber(); + + int getTtl(); +} diff --git a/src/main/java/li/cil/oc2/api/inet/InternetProvider.java b/src/main/java/li/cil/oc2/api/inet/InternetProvider.java new file mode 100644 index 000000000..993e16892 --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/InternetProvider.java @@ -0,0 +1,35 @@ +package li.cil.oc2.api.inet; + +/** + * Internet access provider for oc2:internet-card item. + * At initialization phase one implementation of this interface will be loaded via {@link java.util.ServiceLoader}. + * If no implementation is found, then the default one will be used instead. + *

+ * It is recommended to not implement this interface directly. + * There are several abstract classes for several levels of TCP/IP stack: + * + *

+ *

+ * Each of these classes implements {@link InternetProvider} and + * asks you to provide an implementation of corresponding TCP/IP layer. + * + * @see LinkLocalLayerInternetProvider + * @see NetworkLayerInternetProvider + * @see TransportLayerInternetProvider + * @see SessionLayerInternetProvider + */ +public interface InternetProvider { + + /** + * This method should provide and implementation of {@link LinkLocalLayer} interface and not fail. + * It will be called once for each loaded internet card. + * + * @return an implementation of {@link LinkLocalLayer} interface + */ + LinkLocalLayer provideInternet(); +} diff --git a/src/main/java/li/cil/oc2/api/inet/LinkLocalLayer.java b/src/main/java/li/cil/oc2/api/inet/LinkLocalLayer.java new file mode 100644 index 000000000..8dde71338 --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/LinkLocalLayer.java @@ -0,0 +1,49 @@ +package li.cil.oc2.api.inet; + +import java.nio.ByteBuffer; + +/** + * Link local or channel TCP/IP layer interface. + *

+ * There is a default link local layer implementation that uses provided {@link NetworkLayer} implementation + * (see {@link NetworkLayerInternetProvider} for more information). + */ +public interface LinkLocalLayer { + /** + * Ethernet frame header size. Consists of two ethernet (MAC) addresses and protocol number. + */ + int FRAME_HEADER_SIZE = 14; + + /** + * Default ethernet frame body size that is used in Linux. + */ + int DEFAULT_MTU = 1500; + + /** + * Default ethernet frame size. + */ + int FRAME_SIZE = FRAME_HEADER_SIZE + DEFAULT_MTU; + + //////////////////////////////////////////////////////////////////// + + /** + * Tries to get the next ethernet frame to send it to virtual computer later. + *

+ * Normally, this method is invoked every game tick in the Internet thread. + * + * @param frame byte buffer where frame body should be put + * @return should return false, if no ethernet frame were gathered, and true otherwise + */ + default boolean receiveEthernetFrame(final ByteBuffer frame) { + return false; + } + + /** + * Sends an ethernet frame from virtual computer. + * + * @param frame byte buffer filled with ethernet frame data + */ + default void sendEthernetFrame(final ByteBuffer frame) { + + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/LinkLocalLayerInternetProvider.java b/src/main/java/li/cil/oc2/api/inet/LinkLocalLayerInternetProvider.java new file mode 100644 index 000000000..e9ebeeae8 --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/LinkLocalLayerInternetProvider.java @@ -0,0 +1,38 @@ +package li.cil.oc2.api.inet; + +import li.cil.oc2.common.inet.NullLinkLocalLayer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * An {@link InternetProvider} partial implementation that expects an {@link LinkLocalLayer} implementation from + * protected method {@link LinkLocalLayerInternetProvider#provideLinkLocalLayer()}. + * + * @see InternetProvider + * @see LinkLocalLayer + */ +public abstract class LinkLocalLayerInternetProvider implements InternetProvider { + private static final Logger LOGGER = LogManager.getLogger(); + + /** + * This method is called from {@link LinkLocalLayerInternetProvider#provideInternet()} in order to get an + * {@link LinkLocalLayer} implementation. + * If it fails, then a dummy {@link LinkLocalLayer} implementation will be used. + * Dummy implementation will do nothing. + * + * @return an implementation of link local TCP/IP layer for internet cards + * @throws Exception this method is allowed to fail with an exception + */ + protected abstract LinkLocalLayer provideLinkLocalLayer() throws Exception; + + @Override + public final LinkLocalLayer provideInternet() { + try { + return provideLinkLocalLayer(); + } catch (Exception e) { + LOGGER.error("Failed to instantiate the implementation " + + "of Internet provider for internet card (provider {})", this.getClass(), e); + return NullLinkLocalLayer.INSTANCE; + } + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/NetworkLayer.java b/src/main/java/li/cil/oc2/api/inet/NetworkLayer.java new file mode 100644 index 000000000..7ba335cbe --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/NetworkLayer.java @@ -0,0 +1,65 @@ +package li.cil.oc2.api.inet; + +import java.nio.ByteBuffer; + +/** + * Network TCP/IP layer interface. + *

+ * There is a default network layer implementation that uses provided {@link TransportLayer} implementation + * (see {@link TransportLayerInternetProvider} for more information). + */ +public interface NetworkLayer { + + /** + * The value of this constant should be returned by {@link NetworkLayer#receivePacket(ByteBuffer)} method if no + * data is arrived. + */ + short PROTOCOL_NONE = 0; + + /** + * The value of this constant should be returned by {@link NetworkLayer#receivePacket(ByteBuffer)} method if IPv4 + * packet is arrived. + */ + short PROTOCOL_IPv4 = 0x0800; + + /** + * The value of this constant should be returned by {@link NetworkLayer#receivePacket(ByteBuffer)} method if any IP + * packet is arrived (can be either IPv4 packet or IPv6 packet). + *

+ * Normally, this value should be returned if any data is arrived. + */ + short PROTOCOL_IP = PROTOCOL_IPv4; + + /** + * The value of this constant should be returned by {@link NetworkLayer#receivePacket(ByteBuffer)} method if IPv6 + * packet is arrived. + */ + short PROTOCOL_IPv6 = (short) 0x86dd; + + //////////////////////////////////////////////////////////////////////////////////// + + /** + * Tries to get the next IP paket to wrap it into an ethernet frame and send it to virtual computer later. + *

+ * Normally, this method is invoked every game tick in the Internet thread. + * + * @param packet byte buffer where IP packet body should be put + * @return protocol number of received data (either {@link NetworkLayer#PROTOCOL_IP}, + * {@link NetworkLayer#PROTOCOL_IPv4} or {@link NetworkLayer#PROTOCOL_IPv6}) or + * {@link NetworkLayer#PROTOCOL_NONE}, if no new data has arrived + */ + default short receivePacket(final ByteBuffer packet) { + return PROTOCOL_NONE; + } + + /** + * Sends an IP packet extracted from an ethernet frame that sent virtual computer. + * + * @param protocol protocol number of arrived message; normally, should be either + * {@link NetworkLayer::PROTOCOL_IPv4} or {@link NetworkLayer::PROTOCOL_IPv6} + * @param packet arrived data + */ + default void sendPacket(final short protocol, final ByteBuffer packet) { + + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/NetworkLayerInternetProvider.java b/src/main/java/li/cil/oc2/api/inet/NetworkLayerInternetProvider.java new file mode 100644 index 000000000..c6ae57c8b --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/NetworkLayerInternetProvider.java @@ -0,0 +1,29 @@ +package li.cil.oc2.api.inet; + +import li.cil.oc2.common.inet.DefaultLinkLocalLayer; + +/** + * An {@link InternetProvider} partial implementation that expects an {@link NetworkLayer} implementation from + * protected method {@link NetworkLayerInternetProvider#provideNetworkLayer()}. + * + * @see InternetProvider + * @see NetworkLayer + */ +public abstract class NetworkLayerInternetProvider extends LinkLocalLayerInternetProvider { + + /** + * This method is called from {@link NetworkLayerInternetProvider#provideLinkLocalLayer()} in order to get a + * {@link NetworkLayer} implementation. + * Retrieved {@link NetworkLayer} implementation will be wrapped with internal {@link LinkLocalLayer} + * implementation. + * + * @return an implementation of network TCP/IP layer for internet cards + * @throws Exception this method is allowed to fail with an exception + */ + protected abstract NetworkLayer provideNetworkLayer() throws Exception; + + @Override + protected final LinkLocalLayer provideLinkLocalLayer() throws Exception { + return new DefaultLinkLocalLayer(provideNetworkLayer()); + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/Session.java b/src/main/java/li/cil/oc2/api/inet/Session.java new file mode 100644 index 000000000..946719834 --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/Session.java @@ -0,0 +1,23 @@ +package li.cil.oc2.api.inet; + +import javax.annotation.Nullable; +import java.net.InetSocketAddress; + +public interface Session { + long getId(); + + void close(); + + States getState(); + + @Nullable + Object getUserdata(); + + void setUserdata(final Object userdata); + + InetSocketAddress getDestination(); + + enum States { + NEW, ESTABLISHED, FINISH, REJECT, EXPIRED + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/SessionLayer.java b/src/main/java/li/cil/oc2/api/inet/SessionLayer.java new file mode 100644 index 000000000..7916cc2ca --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/SessionLayer.java @@ -0,0 +1,22 @@ +package li.cil.oc2.api.inet; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; + +/** + * A session layer interface of TCP/IP stack. + */ +public interface SessionLayer { + default void receiveSession(final Receiver receiver) { + + } + + default void sendSession(final Session session, @Nullable final ByteBuffer data) { + session.close(); + } + + interface Receiver { + @Nullable + ByteBuffer receive(Session session); + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/SessionLayerInternetProvider.java b/src/main/java/li/cil/oc2/api/inet/SessionLayerInternetProvider.java new file mode 100644 index 000000000..62fe2909a --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/SessionLayerInternetProvider.java @@ -0,0 +1,29 @@ +package li.cil.oc2.api.inet; + +import li.cil.oc2.common.inet.DefaultTransportLayer; + +/** + * An {@link InternetProvider} partial implementation that expects an {@link SessionLayer} implementation from + * protected method {@link SessionLayerInternetProvider#provideSessionLayer()}. + * + * @see InternetProvider + * @see SessionLayer + */ +public abstract class SessionLayerInternetProvider extends TransportLayerInternetProvider { + + /** + * This method is called from {@link SessionLayerInternetProvider#provideTransportLayer()} in order to get a + * {@link SessionLayer} implementation. + * Retrieved {@link SessionLayer} implementation will be wrapped with internal {@link TransportLayer} + * implementation. + * + * @return an implementation of session TCP/IP layer for internet cards + * @throws Exception this method is allowed to fail with an exception + */ + protected abstract SessionLayer provideSessionLayer() throws Exception; + + @Override + protected final TransportLayer provideTransportLayer() throws Exception { + return new DefaultTransportLayer(provideSessionLayer()); + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/StreamSession.java b/src/main/java/li/cil/oc2/api/inet/StreamSession.java new file mode 100644 index 000000000..739753a62 --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/StreamSession.java @@ -0,0 +1,4 @@ +package li.cil.oc2.api.inet; + +public interface StreamSession extends Session { +} diff --git a/src/main/java/li/cil/oc2/api/inet/TransportLayer.java b/src/main/java/li/cil/oc2/api/inet/TransportLayer.java new file mode 100644 index 000000000..233f0f612 --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/TransportLayer.java @@ -0,0 +1,60 @@ +package li.cil.oc2.api.inet; + +/** + * Transport TCP/IP layer interface. + *

+ * There is a default transport layer implementation that uses provided {@link SessionLayer} implementation + * (see {@link SessionLayerInternetProvider} for more information). + */ +public interface TransportLayer { + + /** + * The value of this constant should be returned by {@link TransportLayer#receiveTransportMessage(TransportMessage)} + * method if no data is arrived. + */ + byte PROTOCOL_NONE = 0; + + /** + * The value of this constant should be returned by {@link TransportLayer#receiveTransportMessage(TransportMessage)} + * method if an ICMP message is arrived. + */ + byte PROTOCOL_ICMP = 1; + + /** + * The value of this constant should be returned by {@link TransportLayer#receiveTransportMessage(TransportMessage)} + * method if a TCP packet is arrived. + */ + byte PROTOCOL_TCP = 6; + + /** + * The value of this constant should be returned by {@link TransportLayer#receiveTransportMessage(TransportMessage)} + * method if a UDP message is arrived. + */ + byte PROTOCOL_UDP = 17; + + //////////////////////////////////////////////////////////////////////////////////// + + /** + * Tries to get the next transport message to wrap it into an IP packet and send it to virtual computer later. + *

+ * Normally, this method is invoked every game tick in the Internet thread. + * + * @param message transport message object that should be filled with arrived data + * @return protocol number of received data (can be, for example, {@link TransportLayer#PROTOCOL_ICMP}, + * {@link TransportLayer#PROTOCOL_TCP} or {@link TransportLayer#PROTOCOL_UDP}) or + * {@link TransportLayer#PROTOCOL_NONE}, if no new data has arrived + */ + default byte receiveTransportMessage(final TransportMessage message) { + return 0; + } + + /** + * Sends a transport message extracted from an IP packet that sent virtual computer. + * + * @param protocol protocol number of arrived message + * @param message arrived transport message + */ + default void sendTransportMessage(byte protocol, final TransportMessage message) { + + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/TransportLayerInternetProvider.java b/src/main/java/li/cil/oc2/api/inet/TransportLayerInternetProvider.java new file mode 100644 index 000000000..77a754f1e --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/TransportLayerInternetProvider.java @@ -0,0 +1,29 @@ +package li.cil.oc2.api.inet; + +import li.cil.oc2.common.inet.DefaultNetworkLayer; + +/** + * An {@link InternetProvider} partial implementation that expects an {@link TransportLayer} implementation from + * protected method {@link TransportLayerInternetProvider#provideTransportLayer()}. + * + * @see InternetProvider + * @see TransportLayer + */ +public abstract class TransportLayerInternetProvider extends NetworkLayerInternetProvider { + + /** + * This method is called from {@link TransportLayerInternetProvider#provideNetworkLayer()} in order to get a + * {@link TransportLayer} implementation. + * Retrieved {@link TransportLayer} implementation will be wrapped with internal {@link NetworkLayer} + * implementation. + * + * @return an implementation of transport TCP/IP layer for internet cards + * @throws Exception this method is allowed to fail with an exception + */ + protected abstract TransportLayer provideTransportLayer() throws Exception; + + @Override + protected final NetworkLayer provideNetworkLayer() throws Exception { + return new DefaultNetworkLayer(provideTransportLayer()); + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/TransportMessage.java b/src/main/java/li/cil/oc2/api/inet/TransportMessage.java new file mode 100644 index 000000000..faa54657a --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/TransportMessage.java @@ -0,0 +1,190 @@ +package li.cil.oc2.api.inet; + +import li.cil.oc2.common.inet.InetUtils; + +import java.net.InetAddress; +import java.nio.ByteBuffer; + +/** + * Reusable data object, that contains information about transport layer message that makes sense both for + * transport and network layer. + */ +public final class TransportMessage { + + private static final byte DEFAULT_TTL = 64; + + /////////////////////////////////////////////////////////////////////// + + private short networkProtocolNumber = -1; + private long srcIpAddressMost = -1; + private long srcIpAddressLeast = -1; + private long dstIpAddressMost = -1; + private long dstIpAddressLeast = -1; + private byte ttl = -1; + private ByteBuffer data = null; + + /////////////////////////////////////////////////////////////////////// + + /** + * Network layer should provide byte buffer for transport layer via this method. + * + * @param data byte buffer for transport layer data + */ + public void initializeBuffer(final ByteBuffer data) { + this.data = data; + } + + /** + * Updates network layer parameters for current transport message. + * + * @param networkProtocolNumber chosen network protocol number (use {@link NetworkLayer#PROTOCOL_IPv4} or + * {@link NetworkLayer#PROTOCOL_IPv6} values) + * @param srcIpAddressMost part of a source IP address + * @param srcIpAddressLeast part of a source IP address + * @param dstIpAddressMost part of a destination IP address + * @param dstIpAddressLeast part of a destination IP address + * @param ttl time to live value (for tracert functionality) + */ + public void update( + final short networkProtocolNumber, + final long srcIpAddressMost, + final long srcIpAddressLeast, + final long dstIpAddressMost, + final long dstIpAddressLeast, + final byte ttl + ) { + this.networkProtocolNumber = networkProtocolNumber; + this.srcIpAddressMost = srcIpAddressMost; + this.srcIpAddressLeast = srcIpAddressLeast; + this.dstIpAddressMost = dstIpAddressMost; + this.dstIpAddressLeast = dstIpAddressLeast; + this.ttl = ttl; + } + + /** + * Updates network layer parameters for current transport message assuming IPv4 network protocol. + * + * @param srcIpAddress source IP address + * @param dstIpAddress destination IP address + * @param ttl time to live value (for tracert functionality) + */ + public void updateIpv4( + final int srcIpAddress, + final int dstIpAddress, + final byte ttl + ) { + update(NetworkLayer.PROTOCOL_IPv4, 0, srcIpAddress, 0, dstIpAddress, ttl); + } + + /** + * Updates network layer parameters for current transport message and sets the default TTL value assuming IPv4 + * network protocol. + * + * @param srcIpAddress source IP address + * @param dstIpAddress destination IP address + */ + public void updateIpv4( + final int srcIpAddress, + final int dstIpAddress + ) { + updateIpv4(srcIpAddress, dstIpAddress, DEFAULT_TTL); + } + + /** + * Gets stored TTL value. + * + * @return TTL value + */ + public byte getTtl() { + return ttl; + } + + /** + * Gets stored source IPv4 address. + * + * @return IPv4 source address + */ + public int getSrcIpv4Address() { + return (int) srcIpAddressLeast; + } + + /** + * Gets stored destination IPv4 address. + * + * @return IPv4 destination address + */ + public int getDstIpv4Address() { + return (int) dstIpAddressLeast; + } + + /** + * Gets stored source IP address as {@link InetAddress} object. + * + * @return IPv4 source address + */ + public InetAddress getSrcAddress() { + switch (networkProtocolNumber) { + case NetworkLayer.PROTOCOL_IPv4: + return InetUtils.toJavaInetAddress(getSrcIpv4Address()); + case NetworkLayer.PROTOCOL_IPv6: + return InetUtils.toJavaInetAddress(srcIpAddressMost, srcIpAddressLeast); + default: + throw new IllegalStateException(); + } + } + + /** + * Gets stored destination IP address as {@link InetAddress} object. + * + * @return IPv4 destination address + */ + public InetAddress getDstAddress() { + switch (networkProtocolNumber) { + case NetworkLayer.PROTOCOL_IPv4: + return InetUtils.toJavaInetAddress(getDstIpv4Address()); + case NetworkLayer.PROTOCOL_IPv6: + return InetUtils.toJavaInetAddress(dstIpAddressMost, dstIpAddressLeast); + default: + throw new IllegalStateException(); + } + } + + /** + * Gets transport layer data buffer + * + * @return transport layer data buffer + */ + public ByteBuffer getData() { + if (data == null) { + throw new IllegalStateException(); + } + return data; + } + + /** + * Gets network protocol number + * + * @return network protocol number + */ + public short getNetworkProtocolNumber() { + return networkProtocolNumber; + } + + /** + * Checks if an IPv4 transport message stored in this object. + * + * @return true only if it is an IPv4 message + */ + public boolean isIpv4() { + return networkProtocolNumber == NetworkLayer.PROTOCOL_IPv4; + } + + /** + * Checks if an IPv6 transport message stored in this object. + * + * @return true only if it is an IPv6 message + */ + public boolean isIpv6() { + return networkProtocolNumber == NetworkLayer.PROTOCOL_IPv6; + } +} diff --git a/src/main/java/li/cil/oc2/api/inet/package-info.java b/src/main/java/li/cil/oc2/api/inet/package-info.java new file mode 100644 index 000000000..ef3f4d071 --- /dev/null +++ b/src/main/java/li/cil/oc2/api/inet/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package li.cil.oc2.api.inet; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/src/main/java/li/cil/oc2/common/CommonSetup.java b/src/main/java/li/cil/oc2/common/CommonSetup.java index 5ef0e195d..f4cac3f16 100644 --- a/src/main/java/li/cil/oc2/common/CommonSetup.java +++ b/src/main/java/li/cil/oc2/common/CommonSetup.java @@ -4,6 +4,7 @@ import li.cil.oc2.common.bus.device.rpc.RPCItemStackTagFilters; import li.cil.oc2.common.bus.device.rpc.RPCMethodParameterTypeAdapters; import li.cil.oc2.common.capabilities.Capabilities; +import li.cil.oc2.common.inet.InternetManager; import li.cil.oc2.common.integration.IMC; import li.cil.oc2.common.network.Network; import li.cil.oc2.common.serialization.BlobStorage; @@ -25,6 +26,7 @@ public static void handleSetupEvent(final FMLCommonSetupEvent event) { RPCItemStackTagFilters.initialize(); RPCMethodParameterTypeAdapters.initialize(); ServerScheduler.initialize(); + InternetManager.initialize(); MinecraftForge.EVENT_BUS.addListener(CommonSetup::handleServerAboutToStart); MinecraftForge.EVENT_BUS.addListener(CommonSetup::handleServerStopped); diff --git a/src/main/java/li/cil/oc2/common/Config.java b/src/main/java/li/cil/oc2/common/Config.java index eade3a891..579254270 100644 --- a/src/main/java/li/cil/oc2/common/Config.java +++ b/src/main/java/li/cil/oc2/common/Config.java @@ -37,6 +37,18 @@ public final class Config { @Path("admin") public static UUID fakePlayerUUID = UUID.fromString("e39dd9a7-514f-4a2d-aa5e-b6030621416d"); + @Path("internet-card") public static boolean internetCardEnabled = false; + @Path("internet-card") public static int defaultSessionLifetimeMs = 60 * 1000; + @Path("internet-card") public static int defaultSessionsNumberPerCardLimit = 10; + @Path("internet-card") public static int defaultSessionsNumberLimit = 100; + @Path("internet-card") public static int defaultEchoRequestTimeoutMs = 1000; + @Path("internet-card") public static String deniedHosts = + "127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4"; + @Path("internet-card") public static String allowedHosts = ""; + @Path("internet-card") public static String defaultNameServer = "1.1.1.1"; + @Path("internet-card") public static boolean useSynchronisedNAT = false; + @Path("internet-card") public static int streamBufferSize = 2000; + public static boolean computersUseEnergy() { return computerEnergyPerTick > 0 && computerEnergyStorage > 0; } diff --git a/src/main/java/li/cil/oc2/common/ConfigManager.java b/src/main/java/li/cil/oc2/common/ConfigManager.java index 2ed81b159..f5a658ddd 100644 --- a/src/main/java/li/cil/oc2/common/ConfigManager.java +++ b/src/main/java/li/cil/oc2/common/ConfigManager.java @@ -64,6 +64,7 @@ public final class ConfigManager { PARSERS.put(double.class, ConfigManager::parseDoubleField); PARSERS.put(String.class, ConfigManager::parseStringField); PARSERS.put(UUID.class, ConfigManager::parseUUIDField); + PARSERS.put(boolean.class, ConfigManager::parseBooleanField); } /////////////////////////////////////////////////////////////////// @@ -164,6 +165,14 @@ private static ConfigFieldPair parseUUIDField(final Object instance, final Fi return new ConfigFieldPair<>(field, configValue, UUID::fromString); } + private static ConfigFieldPair parseBooleanField(final Object instance, final Field field, final String path, final ForgeConfigSpec.Builder builder) throws IllegalAccessException { + final boolean defaultValue = (boolean) field.get(instance); + + final ForgeConfigSpec.BooleanValue configValue = builder.define(path, defaultValue); + + return new ConfigFieldPair<>(field, configValue); + } + private static String getPath(@Nullable final String prefix, final Field field) { return (prefix != null ? prefix + "." : "") + field.getName(); } diff --git a/src/main/java/li/cil/oc2/common/bus/device/item/InternetCardItemDevice.java b/src/main/java/li/cil/oc2/common/bus/device/item/InternetCardItemDevice.java new file mode 100644 index 000000000..9c3f9577f --- /dev/null +++ b/src/main/java/li/cil/oc2/common/bus/device/item/InternetCardItemDevice.java @@ -0,0 +1,143 @@ +package li.cil.oc2.common.bus.device.item; + +import com.google.common.eventbus.Subscribe; +import li.cil.oc2.api.bus.device.ItemDevice; +import li.cil.oc2.api.bus.device.vm.VMDevice; +import li.cil.oc2.api.bus.device.vm.VMDeviceLoadResult; +import li.cil.oc2.api.bus.device.vm.context.VMContext; +import li.cil.oc2.api.bus.device.vm.event.VMPausingEvent; +import li.cil.oc2.api.bus.device.vm.event.VMResumingRunningEvent; +import li.cil.oc2.common.bus.device.util.IdentityProxy; +import li.cil.oc2.common.bus.device.util.OptionalAddress; +import li.cil.oc2.common.bus.device.util.OptionalInterrupt; +import li.cil.oc2.common.inet.InternetAccess; +import li.cil.oc2.common.inet.InternetManager; +import li.cil.oc2.common.serialization.NBTSerialization; +import li.cil.oc2.common.util.NBTTagIds; +import li.cil.sedna.device.virtio.VirtIONetworkDevice; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundNBT; + +import javax.annotation.Nullable; + +@SuppressWarnings("UnstableApiUsage") +public final class InternetCardItemDevice extends IdentityProxy implements VMDevice, ItemDevice { + private static final String DEVICE_TAG_NAME = "device"; + private static final String ADDRESS_TAG_NAME = "address"; + private static final String INTERRUPT_TAG_NAME = "interrupt"; + + /////////////////////////////////////////////////////////////// + private final InternetAccess internetAccess = new InternetAccessImpl(); + private final OptionalAddress address = new OptionalAddress(); + private final OptionalInterrupt interrupt = new OptionalInterrupt(); + private VirtIONetworkDevice device; + private boolean isRunning; + private CompoundNBT deviceTag; + + /////////////////////////////////////////////////////////////// + + public InternetCardItemDevice(final ItemStack identity) { + super(identity); + } + + /////////////////////////////////////////////////////////////// + + @Override + public VMDeviceLoadResult mount(final VMContext context) { + device = new VirtIONetworkDevice(context.getMemoryMap()); + + if (!address.claim(context, device)) { + return VMDeviceLoadResult.fail(); + } + + if (interrupt.claim(context)) { + device.getInterrupt().set(interrupt.getAsInt(), context.getInterruptController()); + } else { + return VMDeviceLoadResult.fail(); + } + + if (deviceTag != null) { + NBTSerialization.deserialize(deviceTag, device); + } + + context.getEventBus().register(this); + + return VMDeviceLoadResult.success(); + } + + @Override + public void unmount() { + suspend(); + isRunning = false; + address.clear(); + interrupt.clear(); + } + + @Override + public void suspend() { + device = null; + } + + @Subscribe + public void handlePausingEvent(final VMPausingEvent event) { + isRunning = false; + } + + @Subscribe + public void handleResumingRunningEvent(final VMResumingRunningEvent event) { + isRunning = true; + InternetManager.connect(internetAccess); + } + + @Override + public CompoundNBT serializeNBT() { + final CompoundNBT tag = new CompoundNBT(); + + if (device != null) { + deviceTag = NBTSerialization.serialize(device); + } + if (deviceTag != null) { + tag.put(DEVICE_TAG_NAME, deviceTag); + } + if (address.isPresent()) { + tag.putLong(ADDRESS_TAG_NAME, address.getAsLong()); + } + if (interrupt.isPresent()) { + tag.putInt(INTERRUPT_TAG_NAME, interrupt.getAsInt()); + } + + return tag; + } + + @Override + public void deserializeNBT(final CompoundNBT tag) { + if (tag.contains(DEVICE_TAG_NAME, NBTTagIds.TAG_COMPOUND)) { + deviceTag = tag.getCompound(DEVICE_TAG_NAME); + } + if (tag.contains(ADDRESS_TAG_NAME, NBTTagIds.TAG_LONG)) { + address.set(tag.getLong(ADDRESS_TAG_NAME)); + } + if (tag.contains(INTERRUPT_TAG_NAME, NBTTagIds.TAG_INT)) { + interrupt.set(tag.getInt(INTERRUPT_TAG_NAME)); + } + } + + private class InternetAccessImpl implements InternetAccess { + + @Nullable + @Override + public byte[] receiveEthernetFrame() { + return device.readEthernetFrame(); + } + + @Override + public void sendEthernetFrame(final byte[] frame) { + device.writeEthernetFrame(frame); + } + + @Override + public boolean isValid() { + return isRunning && device != null; + } + } +} diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java b/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java index 8088b9fd9..67f5a9ede 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java @@ -39,6 +39,7 @@ public static void initialize() { ITEM_DEVICE_PROVIDERS.register("flash_memory_custom", FlashMemoryWithExternalDataItemDeviceProvider::new); ITEM_DEVICE_PROVIDERS.register("redstone_interface_card", RedstoneInterfaceCardItemDeviceProvider::new); ITEM_DEVICE_PROVIDERS.register("network_interface_card", NetworkInterfaceCardItemDeviceProvider::new); + ITEM_DEVICE_PROVIDERS.register("internet_card", InternetCardItemDeviceProvider::new); ITEM_DEVICE_PROVIDERS.register("file_import_export_card", FileImportExportCardItemDeviceProvider::new); ITEM_DEVICE_PROVIDERS.register("sound_card", SoundCardItemDeviceProvider::new); diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/item/InternetCardItemDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/item/InternetCardItemDeviceProvider.java new file mode 100644 index 000000000..0c4b4a901 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/item/InternetCardItemDeviceProvider.java @@ -0,0 +1,26 @@ +package li.cil.oc2.common.bus.device.provider.item; + +import li.cil.oc2.api.bus.device.ItemDevice; +import li.cil.oc2.api.bus.device.provider.ItemDeviceQuery; +import li.cil.oc2.common.Config; +import li.cil.oc2.common.bus.device.item.InternetCardItemDevice; +import li.cil.oc2.common.bus.device.provider.util.AbstractItemDeviceProvider; +import li.cil.oc2.common.item.Items; + +import java.util.Optional; + +public final class InternetCardItemDeviceProvider extends AbstractItemDeviceProvider { + public InternetCardItemDeviceProvider() { + super(Items.INTERNET_CARD); + } + + @Override + protected Optional getItemDevice(final ItemDeviceQuery query) { + return query.getContainerTileEntity().map(tileEntity -> new InternetCardItemDevice(query.getItemStack())); + } + + @Override + protected int getItemDeviceEnergyConsumption(final ItemDeviceQuery query) { + return Config.networkInterfaceEnergyPerTick; + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/DatagramSessionDiscriminator.java b/src/main/java/li/cil/oc2/common/inet/DatagramSessionDiscriminator.java new file mode 100644 index 000000000..698d4376e --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/DatagramSessionDiscriminator.java @@ -0,0 +1,12 @@ +package li.cil.oc2.common.inet; + +final class DatagramSessionDiscriminator extends SocketSessionDiscriminator { + public DatagramSessionDiscriminator( + final int srcIpAddress, + final short srcPort, + final int dstIpAddress, + final short dstPort + ) { + super(srcIpAddress, srcPort, dstIpAddress, dstPort); + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/DatagramSessionImpl.java b/src/main/java/li/cil/oc2/common/inet/DatagramSessionImpl.java new file mode 100644 index 000000000..936c9e92a --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/DatagramSessionImpl.java @@ -0,0 +1,17 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.DatagramSession; + +public final class DatagramSessionImpl extends SessionBase implements DatagramSession { + private final DatagramSessionDiscriminator discriminator; + + public DatagramSessionImpl(final int ipAddress, final short port, final DatagramSessionDiscriminator discriminator) { + super(ipAddress, port); + this.discriminator = discriminator; + } + + @Override + public DatagramSessionDiscriminator getDiscriminator() { + return discriminator; + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/DefaultInternetProvider.java b/src/main/java/li/cil/oc2/common/inet/DefaultInternetProvider.java new file mode 100644 index 000000000..bc1497b69 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/DefaultInternetProvider.java @@ -0,0 +1,19 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.SessionLayer; +import li.cil.oc2.api.inet.SessionLayerInternetProvider; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class DefaultInternetProvider extends SessionLayerInternetProvider { + public static final DefaultInternetProvider INSTANCE = new DefaultInternetProvider(); + private static final Logger LOGGER = LogManager.getLogger(); + + private DefaultInternetProvider() { + } + + @Override + protected SessionLayer provideSessionLayer() { + return new DefaultSessionLayer(); + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/DefaultLinkLocalLayer.java b/src/main/java/li/cil/oc2/common/inet/DefaultLinkLocalLayer.java new file mode 100644 index 000000000..5b7dc9854 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/DefaultLinkLocalLayer.java @@ -0,0 +1,164 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.LinkLocalLayer; +import li.cil.oc2.api.inet.NetworkLayer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.nio.ByteBuffer; +import java.util.Random; + +public final class DefaultLinkLocalLayer implements LinkLocalLayer { + private static final Logger LOGGER = LogManager.getLogger(); + private static final Random random = new Random(); + + /////////////////////////////////////////////////// + + private static final short MAC_PREFIX = 0x5ed1; + + private static final short PROTOCOL_ARP = 0x0806; + + private static final short HW_TYPE_ETHERNET = 0x0001; + + private static final int ARP_MESSAGE_SIZE = 28; + private static final int ARP_ADDRESS_TYPE = (HW_TYPE_ETHERNET << 16) | NetworkLayer.PROTOCOL_IPv4; + private static final short ARP_ADDRESSES_SIZES = (6 << 8) | 4; + + private static final short ARP_REQUEST = 0x0001; + private static final short ARP_RESPONSE = 0x0002; + + private static final int IP_VER4 = 4; // obviously + private static final int IP_VER6 = 6; // obviously + + /////////////////////////////////////////////////// + + private final NetworkLayer networkLayer; + + private final int myMacAddress; + private int myIpAddress = -1; + + private short cardMacPrefix = -1; + private int cardMacAddress = -1; + private int cardIpAddress = -1; + + private boolean needArpResponse = false; + + /////////////////////////////////////////////////// + + public DefaultLinkLocalLayer(final NetworkLayer networkLayer) { + this.networkLayer = networkLayer; + myMacAddress = random.nextInt(); + } + + private void prepareEthernetHeader(final ByteBuffer frame, final short protocol) { + // Prepare ethernet header + frame.putShort(cardMacPrefix); + frame.putInt(cardMacAddress); + frame.putShort(MAC_PREFIX); + frame.putInt(myMacAddress); + frame.putShort(protocol); + } + + @Override + public boolean receiveEthernetFrame(final ByteBuffer frame) { + if (needArpResponse) { + // Make ARP response before anything else + needArpResponse = false; + + prepareEthernetHeader(frame, PROTOCOL_ARP); + + // Prepare ARP response + frame.putInt(ARP_ADDRESS_TYPE); + frame.putShort(ARP_ADDRESSES_SIZES); + frame.putShort(ARP_RESPONSE); + frame.putShort(MAC_PREFIX); + frame.putInt(myMacAddress); + frame.putInt(myIpAddress); + frame.putShort(cardMacPrefix); + frame.putInt(cardMacAddress); + frame.putInt(cardIpAddress); + frame.position(frame.position() - FRAME_HEADER_SIZE - ARP_MESSAGE_SIZE); + LOGGER.trace("ARP message sent"); + } else { + // Wrap IP message + frame.position(frame.position() + FRAME_HEADER_SIZE); + short protocol = networkLayer.receivePacket(frame); + if (protocol == NetworkLayer.PROTOCOL_NONE) { + return false; + } + if (protocol == NetworkLayer.PROTOCOL_IP) { + // This code block exists to make Network layer implementation a bit easier + final int version = Byte.toUnsignedInt(frame.get(frame.position())) >>> 4; + if (version == IP_VER6) { + protocol = NetworkLayer.PROTOCOL_IPv6; + } + } + frame.position(frame.position() - FRAME_HEADER_SIZE); + prepareEthernetHeader(frame, protocol); + frame.position(frame.position() - FRAME_HEADER_SIZE); + LOGGER.trace("IP message sent"); + } + return true; + } + + @Override + public void sendEthernetFrame(final ByteBuffer frame) { + /// Read ethernet header + if (frame.remaining() < FRAME_HEADER_SIZE) { + LOGGER.trace("Ethernet header too low"); + return; + } + // Get destination + final short dstMacPrefix = frame.getShort(); + final int dstMacAddress = frame.getInt(); + // Get source + final short srcMacPrefix = frame.getShort(); + final int srcMacAddress = frame.getInt(); + // Get protocol type + final short protocol = frame.getShort(); + + /// Protocol action + if (protocol == PROTOCOL_ARP) { + LOGGER.trace("ARP message received"); + /// ARP message verification + if (frame.remaining() < ARP_MESSAGE_SIZE) { + return; + } + final int hwAndProtocolAddressesTypes = frame.getInt(); + if (hwAndProtocolAddressesTypes != ARP_ADDRESS_TYPE) { + LOGGER.trace("Wrong ARP address type, drop"); + return; + } + final short addressesSizes = frame.getShort(); + if (addressesSizes != ARP_ADDRESSES_SIZES) { + LOGGER.trace("Wrong ARP address size, drop"); + return; + } + final short messageType = frame.getShort(); + if (messageType != ARP_REQUEST) { + LOGGER.trace("Not an ARP request, drop"); + return; + } + final short senderMacPrefix = frame.getShort(); + final int senderMacAddress = frame.getInt(); + if (senderMacPrefix != srcMacPrefix || senderMacAddress != srcMacAddress) { + LOGGER.trace("Wrong sender, drop"); + return; + } + + /// Valid message, extracting useful data + cardIpAddress = frame.getInt(); + // Do not care in target MAC address + frame.getShort(); + frame.getInt(); + myIpAddress = frame.getInt(); + cardMacPrefix = senderMacPrefix; + cardMacAddress = senderMacAddress; + needArpResponse = true; + } else { + LOGGER.trace("Network message received"); + /// Network message forwarding + networkLayer.sendPacket(protocol, frame); + } + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/DefaultNetworkLayer.java b/src/main/java/li/cil/oc2/common/inet/DefaultNetworkLayer.java new file mode 100644 index 000000000..3f5adea9a --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/DefaultNetworkLayer.java @@ -0,0 +1,128 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.NetworkLayer; +import li.cil.oc2.api.inet.TransportLayer; +import li.cil.oc2.api.inet.TransportMessage; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.nio.ByteBuffer; +import java.util.Random; + +public final class DefaultNetworkLayer implements NetworkLayer { + private static final Logger LOGGER = LogManager.getLogger(); + + private static final Random random = new Random(); + + //////////////////////////////////////////////////////////////////////// + + private static final int IPv4_HEADER_SIZE = 20; + private static final int IPv4_VERSION = 4; // obviously... + + //////////////////////////////////////////////////////////////////////// + + private final TransportLayer transportLayer; + + private final TransportMessage inMessage = new TransportMessage(); + private final TransportMessage outMessage = new TransportMessage(); + + public DefaultNetworkLayer(final TransportLayer transportLayer) { + this.transportLayer = transportLayer; + } + + @Override + public short receivePacket(final ByteBuffer packet) { + // Try to receive something + packet.position(packet.position() + IPv4_HEADER_SIZE); + inMessage.initializeBuffer(packet); + final byte protocol = transportLayer.receiveTransportMessage(inMessage); + if (protocol == TransportLayer.PROTOCOL_NONE || !inMessage.isIpv4()) { + return PROTOCOL_NONE; + } + + // Prepare IP packet + final int srcIpAddress = inMessage.getSrcIpv4Address(); + final int dstIpAddress = inMessage.getDstIpv4Address(); + final int bodySize = packet.remaining(); + + packet.position(packet.position() - IPv4_HEADER_SIZE); + packet.put((byte) ((IPv4_VERSION << 4) | 5)); + packet.put((byte) 0); + packet.putShort((short) (IPv4_HEADER_SIZE + bodySize)); + packet.putShort((short) random.nextInt()); + packet.putShort((short) 0); + packet.put(inMessage.getTtl()); + packet.put(protocol); + packet.putShort((short) 0); + packet.putInt(srcIpAddress); + packet.putInt(dstIpAddress); + + // Calculate header checksum + packet.position(packet.position() - IPv4_HEADER_SIZE); + short checksum = InetUtils.rfc1071Checksum(packet, IPv4_HEADER_SIZE); + packet.position(packet.position() - 10); + packet.putShort(checksum); + packet.position(packet.position() + 8 - IPv4_HEADER_SIZE); + + return PROTOCOL_IPv4; + } + + @Override + public void sendPacket(final short protocol, final ByteBuffer packet) { + if (protocol != PROTOCOL_IPv4) { + LOGGER.info("Unsupported network protocol"); + return; + } + if (packet.remaining() < IPv4_HEADER_SIZE) { + LOGGER.info("IP header is too small"); + return; + } + final byte versionAndIhl = packet.get(); + if ((versionAndIhl >>> 4) != IPv4_VERSION) { + LOGGER.info("Invalid protocol version"); + return; + } + final int headerSize = (versionAndIhl & 0xF) * 4; + if (headerSize < IPv4_HEADER_SIZE || packet.remaining() < headerSize) { + LOGGER.info("Invalid header size"); + return; + } + packet.get(); // too hard, ignore + int messageLength = Short.toUnsignedInt(packet.getShort()); + if (packet.remaining() + 4 < messageLength) { + LOGGER.info("Packet size is lower than IP message size"); + return; + } + packet.getShort(); // normally, we don't expect message to be fragmented + short flagsAndFragmentOffset = packet.getShort(); + if (((flagsAndFragmentOffset >>> 13) & 0b101) != 0) { + LOGGER.info("Fragmented packet prohibited (1)"); + return; // no fragments! + } + if ((flagsAndFragmentOffset & 0x1FFF) != 0) { + LOGGER.info("Fragmented packet prohibited (2)"); + return; // no fragments! + } + byte ttl = (byte) (packet.get() - 1); + if (ttl == 0) { + LOGGER.info("Small TTL value"); + return; + } + byte transportProtocol = packet.get(); + packet.getShort(); // I don't think, that we should expect packet corruption in Minecraft + int srcIpAddress = packet.getInt(); + int dstIpAddress = packet.getInt(); + if (!InternetManager.isAllowedToConnect(dstIpAddress)) { + LOGGER.info("Forbidden IP address"); + return; + } + packet.position(packet.position() + headerSize - IPv4_HEADER_SIZE); // skip options + packet.limit(packet.position() + messageLength - headerSize); // set correct limit + + /// Next layer + LOGGER.info("Transport message received"); + outMessage.initializeBuffer(packet); + outMessage.updateIpv4(srcIpAddress, dstIpAddress, ttl); + transportLayer.sendTransportMessage(transportProtocol, outMessage); + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/DefaultSessionLayer.java b/src/main/java/li/cil/oc2/common/inet/DefaultSessionLayer.java new file mode 100644 index 000000000..479e77c7e --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/DefaultSessionLayer.java @@ -0,0 +1,178 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.DatagramSession; +import li.cil.oc2.api.inet.EchoSession; +import li.cil.oc2.api.inet.Session; +import li.cil.oc2.api.inet.SessionLayer; +import li.cil.oc2.common.Config; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +public final class DefaultSessionLayer implements SessionLayer { + private static final Logger LOGGER = LogManager.getLogger(); + + /////////////////////////////////////////////////////////////////// + + private static final Executor executor = Executors + .newSingleThreadExecutor(runnable -> new Thread(runnable, "internet/blocking-session")); + + private static final Selector selector; + + /////////////////////////////////////////////////////////////////// + + static { + Selector newSelector; + try { + newSelector = Selector.open(); + } catch (IOException e) { + LOGGER.error("Failed to open selector", e); + newSelector = null; + } + selector = newSelector; + InternetManager.registerNetworkThreadAction(DefaultSessionLayer::selectAction); + } + + private final AtomicReference echoResponse = new AtomicReference<>(null); + + /////////////////////////////////////////////////////////////////// + private final Set datagramKeys = new HashSet<>(); + + private static void selectAction() { + try { + selector.selectNow(); + } catch (IOException e) { + LOGGER.error(e); + } + } + + private static SelectionKey register( + final SelectableChannel channel, + final Session session, + final int ops + ) throws IOException { + channel.configureBlocking(false); + return channel.register(selector, ops, session); + } + + @Override + public void receiveSession(final Receiver receiver) { + final EchoResponse pending = echoResponse.getAndSet(null); + if (pending != null) { + final ByteBuffer data = receiver.receive(pending.session); + assert data != null; + data.put(pending.payload); + data.flip(); + return; + } + + for (final SelectionKey key : datagramKeys) { + if (key.isReadable()) { + LOGGER.info("Datagram received"); + final DatagramChannel channel = (DatagramChannel) key.channel(); + try { + final DatagramSession session = (DatagramSession) key.attachment(); + final ByteBuffer datagram = receiver.receive(session); + assert datagram != null; + final SocketAddress address = channel.receive(datagram); + if (address == null) { + continue; + } + if (Config.useSynchronisedNAT && !address.equals(session.getDestination())) { + continue; + } + datagram.flip(); + return; + } catch (IOException e) { + LOGGER.error("Trying to read datagram socket", e); + } + } + } + } + + @Override + public void sendSession(final Session session, @Nullable final ByteBuffer data) { + if (session instanceof EchoSession) { + if (data == null) { + return; // session closed due expiration + } + final EchoSession echoSession = (EchoSession) session; + final EchoResponse response = new EchoResponse(data, echoSession); + final InetAddress address = session.getDestination().getAddress(); + executor.execute(() -> { + try { + if (address.isReachable(null, echoSession.getTtl(), Config.defaultEchoRequestTimeoutMs)) { + echoResponse.set(response); + } + } catch (IOException e) { + LOGGER.error("Failed to get echo response", e); + } + }); + } else if (session instanceof DatagramSession) { + try { + switch (session.getState()) { + case NEW: { + final DatagramChannel channel = DatagramChannel.open(); + channel.configureBlocking(false); + final SelectionKey key = + register(channel, session, SelectionKey.OP_READ | SelectionKey.OP_WRITE); + session.setUserdata(key); + datagramKeys.add(key); + LOGGER.info("Open datagram socket {}", session.getDestination()); + /* Fallthrough */ + } + case ESTABLISHED: { + final SelectionKey key = (SelectionKey) session.getUserdata(); + assert key != null; + if (key.isWritable()) { + final DatagramChannel channel = (DatagramChannel) key.channel(); + assert data != null; + channel.send(data, session.getDestination()); + } + break; + } + case EXPIRED: { + final SelectionKey key = (SelectionKey) session.getUserdata(); + assert key != null; + key.channel().close(); + datagramKeys.remove(key); + LOGGER.info("Close datagram socket {}", session.getDestination()); + break; + } + } + } catch (IOException e) { + LOGGER.error("Datagram session failure", e); + session.close(); + } + } else { + session.close(); + } + } + + /////////////////////////////////////////////////////////////////// + + private static final class EchoResponse { + final byte[] payload; + final EchoSession session; + + public EchoResponse(final ByteBuffer payload, final EchoSession session) { + this.payload = new byte[payload.remaining()]; + payload.get(this.payload); + this.session = session; + } + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/DefaultTransportLayer.java b/src/main/java/li/cil/oc2/common/inet/DefaultTransportLayer.java new file mode 100644 index 000000000..ff3d8437f --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/DefaultTransportLayer.java @@ -0,0 +1,366 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.*; +import li.cil.oc2.common.Config; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.*; +import java.util.function.Function; + +public final class DefaultTransportLayer implements TransportLayer { + private static final Logger LOGGER = LogManager.getLogger(); + private static final byte ICMP_TYPE_ECHO_REPLY = 0; + + /////////////////////////////////////////////////////////// + private static final byte ICMP_TYPE_ECHO_REQUEST = 8; + private static final byte ICMP_TYPE_ECHO_UNREACHABLE = 3; + private static final byte ICMP_CODE_ECHO_UNREACHABLE_PORT = 3; + private static final byte ICMP_CODE_ECHO_UNREACHABLE_PROHIBITED = 13; + private static final short PORT_ECHO = 7; + private static final int ICMP_HEADER_SIZE = 8; + private static final int UDP_HEADER_SIZE = 8; + private static int allSessionCount = 0; + + /////////////////////////////////////////////////////////// + private final SessionLayer sessionLayer; + + private final SessionReceiver receiver = new SessionReceiver(); + + private final NavigableMap expirationQueue = new TreeMap<>(); + private final Map, SessionBase> sessions = new HashMap<>(); + + private ICMPReply icmpReply = null; + + /////////////////////////////////////////////////////////// + + public DefaultTransportLayer(final SessionLayer sessionLayer) { + this.sessionLayer = sessionLayer; + } + + private void processExpirationQueue() { + if (expirationQueue.isEmpty()) { + return; + } + final Instant now = Instant.now(); + final Iterator> iterator = expirationQueue.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (entry.getKey().compareTo(now) < 0) { + iterator.remove(); + final SessionBase session = entry.getValue(); + sessions.remove(session.getDiscriminator()); + --allSessionCount; + LOGGER.info("Expired session {}", session.getDiscriminator()); + session.setState(Session.States.EXPIRED); + sessionLayer.sendSession(session, null); + } else { + return; + } + } + } + + private void updateSession(final SessionBase session) { + final Instant oldKey = session.getExpireTime(); + if (oldKey != null) { + expirationQueue.remove(oldKey); + } + session.updateExpireTime(); + final Instant newExpireTime = session.getExpireTime(); + SessionBase previous = expirationQueue.put(newExpireTime, session); + assert previous == null; + } + + private void closeSession(final SessionBase session) { + LOGGER.info("Close session {}", session.getDiscriminator()); + sessions.remove(session.getDiscriminator()); + expirationQueue.remove(session.getExpireTime()); + --allSessionCount; + } + + private void prepareIcmpHeader(final ByteBuffer buffer, final byte type, final byte code) { + final int position = buffer.position(); + buffer.put(type); + buffer.put(code); + buffer.putShort((short) 0); + buffer.position(position); + short checksum = InetUtils.rfc1071Checksum(buffer); + buffer.putShort(position + 2, checksum); + buffer.position(position); + } + + @SuppressWarnings("unchecked") + @Nullable + private > S getOrCreateSession( + final D discriminator, + final Function factory + ) { + final S session = (S) sessions.get(discriminator); + if (session != null) { + return session; + } + if (sessions.size() >= Config.defaultSessionsNumberPerCardLimit) { + LOGGER.warn("Session count per card limit has reached"); + return null; + } + if (allSessionCount >= Config.defaultSessionsNumberLimit) { + LOGGER.warn("Session count limit has reached"); + return null; + } + ++allSessionCount; + LOGGER.info("New session: {}", discriminator); + final S newSession = factory.apply(discriminator); + sessions.put(discriminator, newSession); + updateSession(newSession); + return newSession; + } + + private void reject(final ByteBuffer payload, final int srcIpAddress) { + final byte[] data = InetUtils.quickICMPBody(payload); + icmpReply = new ICMPReply( + ICMP_TYPE_ECHO_UNREACHABLE, + ICMP_CODE_ECHO_UNREACHABLE_PROHIBITED, + 0, + srcIpAddress, + data + ); + } + + private void sessionSendFinish(final SessionBase session, final ByteBuffer payload, final int srcIpAddress) { + final Session.States state = session.getState(); + switch (state) { + case NEW: + session.setState(Session.States.ESTABLISHED); + break; + case REJECT: { + reject(payload, srcIpAddress); + LOGGER.info("Reject session {}", session.getDiscriminator()); + /* Fallthrough */ + } + case FINISH: + closeSession(session); + break; + case ESTABLISHED: + break; + default: + throw new IllegalStateException(state.name()); + } + } + + @Override + public byte receiveTransportMessage(final TransportMessage message) { + processExpirationQueue(); + + while (true) { + if (icmpReply != null) { + message.updateIpv4(icmpReply.srcIpAddress, icmpReply.dstIpAddress); + final ByteBuffer data = message.getData(); + final int position = data.position(); + data.putInt(0); + data.put(icmpReply.payload); + data.limit(data.position()); + data.position(position); + prepareIcmpHeader(data, icmpReply.type, icmpReply.code); + icmpReply = null; + return PROTOCOL_ICMP; + } + + receiver.prepare(message.getData()); + sessionLayer.receiveSession(receiver); + + final SessionBase session = receiver.session; + + if (session == null) { + return PROTOCOL_NONE; + } + + updateSession(session); + + if (session instanceof EchoSession) { + final EchoSessionImpl echoSession = (EchoSessionImpl) session; + switch (session.getState()) { + case FINISH: + closeSession(session); + break; + case ESTABLISHED: { + final EchoSessionDiscriminator discriminator = echoSession.getDiscriminator(); + final ByteBuffer buffer = receiver.getBuffer(); + final int position = buffer.position(); + buffer.putShort(position + 4, discriminator.getIdentity()); + buffer.putShort(position + 6, (short) echoSession.getSequenceNumber()); + prepareIcmpHeader(buffer, ICMP_TYPE_ECHO_REPLY, (byte) 0); + message.updateIpv4(discriminator.getDstIpAddress(), discriminator.getSrcIpAddress()); + return PROTOCOL_ICMP; + } + default: + throw new IllegalStateException(); + } + } else if (session instanceof DatagramSession) { + final DatagramSessionImpl datagramSession = (DatagramSessionImpl) session; + switch (session.getState()) { + case FINISH: + closeSession(session); + break; + case ESTABLISHED: { + final DatagramSessionDiscriminator discriminator = datagramSession.getDiscriminator(); + final ByteBuffer buffer = receiver.getBuffer(); + final int position = buffer.position(); + buffer.putShort(position, discriminator.getDstPort()); + buffer.putShort(position + 2, discriminator.getSrcPort()); + buffer.putShort(position + 4, (short) buffer.remaining()); + buffer.putShort(position + 6, (short) 0); + short checksum = InetUtils.transportRfc1071Checksum( + buffer, + discriminator.getDstIpAddress(), + discriminator.getSrcIpAddress(), + PROTOCOL_UDP + ); + buffer.putShort(position + 6, checksum); + buffer.position(position); + message.updateIpv4(discriminator.getDstIpAddress(), discriminator.getSrcIpAddress()); + return PROTOCOL_UDP; + } + default: + throw new IllegalStateException(); + } + } else if (session instanceof StreamSession) { + throw new UnsupportedOperationException("TODO"); + } else { + throw new IllegalStateException(); + } + } + } + + @Override + public void sendTransportMessage(final byte protocol, final TransportMessage message) { + processExpirationQueue(); + + final int srcIpAddress = message.getSrcIpv4Address(); + final int dstIpAddress = message.getDstIpv4Address(); + + final ByteBuffer data = message.getData(); + + switch (protocol) { + case PROTOCOL_ICMP: { + if (data.remaining() < ICMP_HEADER_SIZE) { + return; + } + + final byte type = data.get(); + final byte code = data.get(); + data.getShort(); // we don't expect incorrect checksum + + if (type == ICMP_TYPE_ECHO_REQUEST) { + if (code != 0) { + return; + } + final short identity = data.getShort(); + final short sequence = data.getShort(); + final EchoSessionDiscriminator discriminator = + new EchoSessionDiscriminator(srcIpAddress, dstIpAddress, identity); + final EchoSessionImpl session = + getOrCreateSession(discriminator, it -> new EchoSessionImpl(dstIpAddress, PORT_ECHO, it)); + if (session == null) { + reject(data, srcIpAddress); + } else { + session.setSequenceNumber(sequence); + session.setTtl(message.getTtl()); + sessionLayer.sendSession(session, data); + sessionSendFinish(session, data, srcIpAddress); + } + } + break; + } + case PROTOCOL_UDP: { + if (data.remaining() < UDP_HEADER_SIZE) { + return; + } + + final short srcPort = data.getShort(); + final short dstPort = data.getShort(); + final int datagramLength = Short.toUnsignedInt(data.getShort()); + data.getShort(); // udp checksum + + if (data.remaining() + UDP_HEADER_SIZE < datagramLength) { + return; + } + data.limit(data.position() + datagramLength - UDP_HEADER_SIZE); + + final DatagramSessionDiscriminator discriminator = + new DatagramSessionDiscriminator(srcIpAddress, srcPort, dstIpAddress, dstPort); + final DatagramSessionImpl session = + getOrCreateSession(discriminator, it -> new DatagramSessionImpl(dstIpAddress, dstPort, it)); + if (session == null) { + reject(data, srcIpAddress); + } else { + sessionLayer.sendSession(session, data); + sessionSendFinish(session, data, srcIpAddress); + } + break; + } + } + } + + private static final class ICMPReply { + private final byte type; + private final byte code; + private final int srcIpAddress; + private final int dstIpAddress; + private final byte[] payload; + + public ICMPReply(final byte type, final byte code, final int srcIpAddress, final int dstIpAddress, final byte[] payload) { + this.type = type; + this.code = code; + this.srcIpAddress = srcIpAddress; + this.dstIpAddress = dstIpAddress; + this.payload = payload; + } + } + + private static final class SessionReceiver implements SessionLayer.Receiver { + private SessionBase session = null; + private ByteBuffer buffer = null; + private int position = 0; + private int limit = 0; + + private void prepare(final ByteBuffer buffer) { + session = null; + this.buffer = buffer; + position = buffer.position(); + limit = buffer.limit(); + } + + private ByteBuffer getBuffer() { + buffer.position(position); + return buffer; + } + + @Nullable + @Override + public ByteBuffer receive(final Session session) { + buffer.position(position); + buffer.limit(limit); + this.session = (SessionBase) session; + switch (session.getState()) { + case NEW: + case FINISH: + case REJECT: + return null; + case ESTABLISHED: + if (session instanceof EchoSession || session instanceof DatagramSession) { + buffer.putLong(0); + return buffer; + } else if (session instanceof StreamSession) { + throw new UnsupportedOperationException("TODO"); + } else { + throw new IllegalArgumentException("session"); + } + default: + throw new IllegalStateException(); + } + } + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/EchoSessionDiscriminator.java b/src/main/java/li/cil/oc2/common/inet/EchoSessionDiscriminator.java new file mode 100644 index 000000000..d9033f056 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/EchoSessionDiscriminator.java @@ -0,0 +1,52 @@ +package li.cil.oc2.common.inet; + +import java.util.Objects; + +final class EchoSessionDiscriminator implements SessionDiscriminator { + private final int srcIpAddress; + private final int dstIpAddress; + private final short identity; + + public EchoSessionDiscriminator(final int srcIpAddress, final int dstIpAddress, final short identity) { + this.srcIpAddress = srcIpAddress; + this.dstIpAddress = dstIpAddress; + this.identity = identity; + } + + public int getSrcIpAddress() { + return srcIpAddress; + } + + public int getDstIpAddress() { + return dstIpAddress; + } + + public short getIdentity() { + return identity; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EchoSessionDiscriminator that = (EchoSessionDiscriminator) o; + return srcIpAddress == that.srcIpAddress && dstIpAddress == that.dstIpAddress && identity == that.identity; + } + + @Override + public int hashCode() { + return Objects.hash(srcIpAddress, dstIpAddress, identity); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder("Echo("); + InetUtils.ipAddressToString(builder, srcIpAddress); + builder.append("<-["); + builder.append(Short.toUnsignedInt(identity)); + builder.append("]->"); + InetUtils.ipAddressToString(builder, dstIpAddress); + builder.append(')'); + return builder.toString(); + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/EchoSessionImpl.java b/src/main/java/li/cil/oc2/common/inet/EchoSessionImpl.java new file mode 100644 index 000000000..7098e3f71 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/EchoSessionImpl.java @@ -0,0 +1,37 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.EchoSession; + +public final class EchoSessionImpl extends SessionBase implements EchoSession { + private final EchoSessionDiscriminator discriminator; + private byte ttl; + private short sequenceNumber; + + public EchoSessionImpl(final int ipAddress, final short port, final EchoSessionDiscriminator discriminator) { + super(ipAddress, port); + this.discriminator = discriminator; + } + + @Override + public int getTtl() { + return Byte.toUnsignedInt(ttl); + } + + public void setTtl(final byte ttl) { + this.ttl = ttl; + } + + @Override + public int getSequenceNumber() { + return Short.toUnsignedInt(sequenceNumber); + } + + public void setSequenceNumber(final short sequenceNumber) { + this.sequenceNumber = sequenceNumber; + } + + @Override + public EchoSessionDiscriminator getDiscriminator() { + return discriminator; + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/InetUtils.java b/src/main/java/li/cil/oc2/common/inet/InetUtils.java new file mode 100644 index 000000000..c117cabec --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/InetUtils.java @@ -0,0 +1,171 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.LinkLocalLayer; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; + +public final class InetUtils { + private static int bufferChecksum(final ByteBuffer buffer, final int size) { + final int halfSize = size >>> 1; + int checksum = 0; + for (int i = 0; i < halfSize; ++i) { + checksum += Short.toUnsignedInt(buffer.getShort()); + } + if ((size & 1) != 0) { + checksum += (buffer.get() << 8) & 0xFFFF; + } + return checksum; + } + + private static short finishChecksum(int checksum) { + checksum = (checksum >>> 16) + (checksum & 0xFFFF); + checksum = (checksum >>> 16) + (checksum & 0xFFFF); + return (short) ~checksum; + } + + public static short rfc1071Checksum(final ByteBuffer buffer, final int size) { + final int checksum = bufferChecksum(buffer, size); + return finishChecksum(checksum); + } + + public static short rfc1071Checksum(final ByteBuffer buffer) { + return rfc1071Checksum(buffer, buffer.remaining()); + } + + public static short transportRfc1071Checksum( + final ByteBuffer buffer, + final int srcIpAddress, + final int dstIpAddress, + final byte protocol + ) { + final int size = buffer.remaining(); + final int checksumPart = bufferChecksum(buffer, size); + final int checksum = checksumPart + Byte.toUnsignedInt(protocol) + size + + (srcIpAddress >>> 16) + (srcIpAddress & 0xFFFF) + + (dstIpAddress >>> 16) + (dstIpAddress & 0xFFFF); + return finishChecksum(checksum); + } + + private static InetAddress getInetAddressByBytes(final byte[] bytes) { + try { + return InetAddress.getByAddress(bytes); + } catch (UnknownHostException e) { + /* should not be there */ + throw new Error("unreachable", e); + } + } + + public static InetAddress toJavaInetAddress(final int ipAddress) { + final byte[] bytes = new byte[]{ + (byte) (ipAddress >>> 24), + (byte) (ipAddress >>> 16), + (byte) (ipAddress >>> 8), + (byte) (ipAddress) + }; + return getInetAddressByBytes(bytes); + } + + private static void fillLong(final byte[] destination, final int offset, final long value) { + for (int position = 0; position < 8; ++position) { + destination[offset + position] = (byte) (value >>> ((7 - position) << 3)); + } + } + + public static InetAddress toJavaInetAddress(final long ipAddressMost, final long ipAddressLeast) { + final byte[] bytes = new byte[16]; + fillLong(bytes, 0, ipAddressMost); + fillLong(bytes, 8, ipAddressLeast); + return getInetAddressByBytes(bytes); + } + + public static void ipAddressToString(final StringBuilder builder, final int ipAddress) { + builder.append(Integer.toUnsignedString(ipAddress >>> 24)); + builder.append('.'); + builder.append(Integer.toUnsignedString((ipAddress >>> 16) & 0xFF)); + builder.append('.'); + builder.append(Integer.toUnsignedString((ipAddress >>> 8) & 0xFF)); + builder.append('.'); + builder.append(Integer.toUnsignedString(ipAddress & 0xFF)); + } + + public static void socketAddressToString(final StringBuilder builder, final int ipAddress, final short port) { + ipAddressToString(builder, ipAddress); + builder.append(':'); + builder.append(Short.toUnsignedInt(port)); + } + + public static byte[] quickICMPBody(final ByteBuffer data) { + final int tmpPosition = data.position(); + final int tmpLimit = data.limit(); + data.limit(data.capacity()); + data.position(LinkLocalLayer.FRAME_HEADER_SIZE); + final int headerSize = (data.get() & 0xF) * 4; + data.position(LinkLocalLayer.FRAME_HEADER_SIZE); + data.limit(LinkLocalLayer.FRAME_HEADER_SIZE + headerSize + 8); + final byte[] result = new byte[data.remaining() + 4]; + result[2] = 0x5; + result[3] = (byte) 0xDC; + data.put(result, 4, data.remaining()); + data.limit(tmpLimit); + data.position(tmpPosition); + return result; + } + + public static int javaInetAddressToIpAddress(final Inet4Address address) { + final byte[] bytes = address.getAddress(); + return (Byte.toUnsignedInt(bytes[0]) << 24) | (Byte.toUnsignedInt(bytes[1]) << 16) + | (Byte.toUnsignedInt(bytes[2]) << 8) | Byte.toUnsignedInt(bytes[3]); + } + + public static int parseIpv4Address(final String string) { + try { + return javaInetAddressToIpAddress((Inet4Address) InetAddress.getByName(string)); + } catch (UnknownHostException e) { + throw new Error("Unreachable", e); + } + } + + public static int getSubnetByPrefix(final int prefix) { + if (prefix > 30 || prefix < 0) { + throw new IllegalArgumentException("Wrong subnet prefix range"); + } + return -1 << (32 - prefix); + } + + private static void configureIpSpace(final Ipv4Space ipSpace, final String hosts) { + int i = 1; + for (final String hostString : hosts.split(",")) { + final String rangeString = hostString.trim(); + if (rangeString.isEmpty()) { + continue; + } + try { + ipSpace.put(rangeString); + } catch (final Exception e) { + throw new IllegalArgumentException("Failed to parse IPv4 address range #" + i + ": " + e.getMessage()); + } + ++i; + } + } + + public static Ipv4Space computeIpSpace(final String deniedHosts, final String allowedHosts) { + final boolean deniedHostsIsEmpty = deniedHosts.trim().isEmpty(); + final boolean allowedHostsIsEmpty = allowedHosts.trim().isEmpty(); + if (deniedHostsIsEmpty && allowedHostsIsEmpty) { + return new Ipv4Space(Ipv4Space.Modes.DENYLIST); + } else if (allowedHostsIsEmpty) { + final Ipv4Space ipSpace = new Ipv4Space(Ipv4Space.Modes.DENYLIST); + configureIpSpace(ipSpace, deniedHosts); + return ipSpace; + } else if (deniedHostsIsEmpty) { + final Ipv4Space ipSpace = new Ipv4Space(Ipv4Space.Modes.ALLOWLIST); + configureIpSpace(ipSpace, allowedHosts); + return ipSpace; + } else { + throw new IllegalArgumentException("Both denied and allowed hosts are specified"); + } + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/InternetAccess.java b/src/main/java/li/cil/oc2/common/inet/InternetAccess.java new file mode 100644 index 000000000..0cca0b4aa --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/InternetAccess.java @@ -0,0 +1,12 @@ +package li.cil.oc2.common.inet; + +import javax.annotation.Nullable; + +public interface InternetAccess { + boolean isValid(); + + @Nullable + byte[] receiveEthernetFrame(); + + void sendEthernetFrame(byte[] frame); +} diff --git a/src/main/java/li/cil/oc2/common/inet/InternetManager.java b/src/main/java/li/cil/oc2/common/inet/InternetManager.java new file mode 100644 index 000000000..0b0a92969 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/InternetManager.java @@ -0,0 +1,170 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.InternetProvider; +import li.cil.oc2.api.inet.LinkLocalLayer; +import li.cil.oc2.common.Config; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.event.server.FMLServerStoppingEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +public final class InternetManager { + private static final Logger LOGGER = LogManager.getLogger(); + + ////////////////////////////////////////////////////////////// + + private static final InternetProvider internetProvider; + private static final Map internetAccesses = new HashMap<>(); + + ////////////////////////////////////////////////////////////// + private static final List actions = new ArrayList<>(); + private static Executor executor; + private static Ipv4Space ipSpace; + + static { + ServiceLoader serviceLoader = ServiceLoader.load(InternetProvider.class); + Iterator iterator = serviceLoader.iterator(); + if (iterator.hasNext()) { + internetProvider = iterator.next(); + } else { + internetProvider = DefaultInternetProvider.INSTANCE; + } + } + + public static void initialize() { + if (!Config.internetCardEnabled) { + LOGGER.info("Internet card is disabled; Internet manager will not start"); + return; + } + LOGGER.warn("Internet card is enabled; Players may access to the internal network"); + executor = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "Internet")); + ipSpace = InetUtils.computeIpSpace(Config.deniedHosts, Config.allowedHosts); + MinecraftForge.EVENT_BUS.register(InternetManager.class); + } + + public static void connect(final InternetAccess internetAccess) { + if (!Config.internetCardEnabled) { + return; + } + if (internetAccesses.containsKey(internetAccess)) { + return; + } + internetAccesses.put(internetAccess, new PendingFrames(internetProvider.provideInternet())); + } + + private static void runCatching(final Runnable action) { + try { + action.run(); + } catch (Exception e) { + LOGGER.error("Uncaught exception", e); + } + } + + private static void processInternetAccess(final Map.Entry entry) { + final PendingFrames frames = entry.getValue(); + final InternetAccess access = entry.getKey(); + final byte[] received = frames.incoming.get(); + if (received != null) { + access.sendEthernetFrame(received); + } + final byte[] sending = access.receiveEthernetFrame(); + if (sending != null) { + frames.outcoming.put(sending); + } + executor.execute(frames); + } + + public static void registerNetworkThreadAction(final Runnable action) { + if (!Config.internetCardEnabled) { + return; + } + actions.add(action); + } + + public static boolean isAllowedToConnect(final int ipAddress) { + return ipSpace.isAllowed(ipAddress); + } + + ////////////////////////////////////////////////////////////// + + @SubscribeEvent + public static void onTick(final TickEvent.ServerTickEvent event) { + executor.execute(() -> actions.forEach(InternetManager::runCatching)); + final Iterator> iterator = internetAccesses.entrySet().iterator(); + while (iterator.hasNext()) { + final Map.Entry entry = iterator.next(); + if (entry.getKey().isValid()) { + processInternetAccess(entry); + } else { + iterator.remove(); + } + } + } + + @SubscribeEvent + public static void onStopping(final FMLServerStoppingEvent event) { + internetAccesses.clear(); + } + + ////////////////////////////////////////////////////////////// + + private static final class PendingFrames implements Runnable { + + public final PendingFrame incoming = new PendingFrame(); + public final PendingFrame outcoming = new PendingFrame(); + + private final ByteBuffer receiveBuffer = ByteBuffer.allocate(LinkLocalLayer.FRAME_SIZE); + private final LinkLocalLayer ethernet; + + ////////////////////////////////////////////////////////////// + + public PendingFrames(final LinkLocalLayer ethernet) { + this.ethernet = ethernet; + } + + @Override + public void run() { + try { + final byte[] outFrame = outcoming.get(); + if (outFrame != null) { + ethernet.sendEthernetFrame(ByteBuffer.wrap(outFrame)); + } + receiveBuffer.clear(); + if (ethernet.receiveEthernetFrame(receiveBuffer)) { + final byte[] inFrame = new byte[receiveBuffer.remaining()]; + receiveBuffer.get(inFrame); + incoming.put(inFrame); + } + } catch (Exception e) { + LOGGER.error("Uncaught exception", e); + } + } + + ////////////////////////////////////////////////////////////// + + private static final class PendingFrame { + + private final AtomicReference pendingFrame = new AtomicReference<>(); + + ////////////////////////////////////////////////////////////// + + @Nullable + public byte[] get() { + return pendingFrame.getAndSet(null); + } + + public void put(final byte[] frame) { + pendingFrame.set(frame); + } + } + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/Ipv4Space.java b/src/main/java/li/cil/oc2/common/inet/Ipv4Space.java new file mode 100644 index 000000000..09adb185f --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/Ipv4Space.java @@ -0,0 +1,134 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.common.util.IntegerSpace; + +import javax.annotation.Nullable; +import javax.annotation.RegEx; +import java.io.IOException; +import java.net.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class Ipv4Space extends IntegerSpace { + + private static final String IPADDRESS_PATTERN = + "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}"; + private static final Pattern ipAddressPattern = + line(group("ip", IPADDRESS_PATTERN)); + private static final Pattern ipRangePattern = + line(group("start", IPADDRESS_PATTERN) + "-" + group("end", IPADDRESS_PATTERN)); + private static final Pattern subnetPattern = + line(group("ip", IPADDRESS_PATTERN) + "\\/" + group("prefix", "[1-9]\\d?")); + private static final Pattern interfaceNamePattern = + line("@" + group("name", "[a-zA-Z].*")); + private static final Pattern interfaceIdPattern = + line("@" + group("id", "\\d*")); + private final boolean isAllowListMode; + + public Ipv4Space(final Modes mode) { + isAllowListMode = mode == Modes.ALLOWLIST; + } + + private static Pattern line(@RegEx final String pattern) { + return Pattern.compile('^' + pattern + '$'); + } + + private static String group(final String name, @RegEx final String pattern) { + return "(?<" + name + ">" + pattern + ")"; + } + + @Override + protected void elementToString(final StringBuilder builder, final int element) { + InetUtils.ipAddressToString(builder, element); + } + + private boolean putSubnet(final int ipAddress, final int prefix) { + final int subnet = InetUtils.getSubnetByPrefix(prefix); + final int rangeStart = ipAddress & subnet; + final int rangeEnd = ipAddress | ~subnet; + return put(rangeStart, rangeEnd); + } + + private boolean putNetworkInterface(@Nullable final NetworkInterface networkInterface) { + if (networkInterface == null) { + throw new IllegalArgumentException("Network interface not found"); + } + boolean result = false; + for (final InterfaceAddress address : networkInterface.getInterfaceAddresses()) { + final InetAddress inetAddress = address.getAddress(); + if (inetAddress instanceof Inet4Address) { + final int ipAddress = InetUtils.javaInetAddressToIpAddress((Inet4Address) inetAddress); + result = putSubnet(ipAddress, address.getNetworkPrefixLength()) || result; + } + } + return result; + } + + public boolean put(final String string) { + final Matcher ipAddressMatch = ipAddressPattern.matcher(string); + if (ipAddressMatch.matches()) { + final int ipAddress = InetUtils.parseIpv4Address(ipAddressMatch.group("ip")); + return put(ipAddress); + } + + final Matcher ipRangeMatch = ipRangePattern.matcher(string); + if (ipRangeMatch.matches()) { + final int rangeStart = InetUtils.parseIpv4Address(ipRangeMatch.group("start")); + final int rangeEnd = InetUtils.parseIpv4Address(ipRangeMatch.group("end")); + return put(rangeStart, rangeEnd); + } + + final Matcher subnetMatch = subnetPattern.matcher(string); + if (subnetMatch.matches()) { + final int ipAddress = InetUtils.parseIpv4Address(subnetMatch.group("ip")); + final int prefix = Integer.parseInt(subnetMatch.group("prefix")); + return putSubnet(ipAddress, prefix); + } + + final Matcher interfaceNameMatch = interfaceNamePattern.matcher(string); + if (interfaceNameMatch.matches()) { + final String interfaceName = interfaceNameMatch.group("name"); + try { + final NetworkInterface networkInterface = NetworkInterface.getByName(interfaceName); + return putNetworkInterface(networkInterface); + } catch (IOException e) { + throw new IllegalStateException("Failed to get a network interface by name"); + } + } + + final Matcher interfaceIdMatch = interfaceIdPattern.matcher(string); + if (interfaceIdMatch.matches()) { + final int interfaceId = Integer.parseInt(interfaceIdMatch.group("id")); + try { + final NetworkInterface networkInterface = NetworkInterface.getByIndex(interfaceId); + return putNetworkInterface(networkInterface); + } catch (IOException e) { + throw new IllegalStateException("Failed to get a network interface by index"); + } + } + + // Assume it is a hostname + try { + final InetAddress[] addresses = InetAddress.getAllByName(string); + boolean result = false; + for (final InetAddress address : addresses) { + if (address instanceof Inet4Address) { + final int ipAddress = InetUtils.javaInetAddressToIpAddress((Inet4Address) address); + result = put(ipAddress) || result; + } + } + return result; + } catch (UnknownHostException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + public boolean isAllowed(final int ipAddress) { + return isAllowListMode == contains(ipAddress); + } + + public enum Modes { + ALLOWLIST, + DENYLIST, + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/NullLinkLocalLayer.java b/src/main/java/li/cil/oc2/common/inet/NullLinkLocalLayer.java new file mode 100644 index 000000000..2301d8670 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/NullLinkLocalLayer.java @@ -0,0 +1,10 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.LinkLocalLayer; + +public final class NullLinkLocalLayer implements LinkLocalLayer { + public static final NullLinkLocalLayer INSTANCE = new NullLinkLocalLayer(); + + private NullLinkLocalLayer() { + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/NullNetworkLayer.java b/src/main/java/li/cil/oc2/common/inet/NullNetworkLayer.java new file mode 100644 index 000000000..908167f12 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/NullNetworkLayer.java @@ -0,0 +1,10 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.NetworkLayer; + +public final class NullNetworkLayer implements NetworkLayer { + public static final NullNetworkLayer INSTANCE = new NullNetworkLayer(); + + private NullNetworkLayer() { + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/NullSessionLayer.java b/src/main/java/li/cil/oc2/common/inet/NullSessionLayer.java new file mode 100644 index 000000000..786aa2b73 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/NullSessionLayer.java @@ -0,0 +1,10 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.SessionLayer; + +public final class NullSessionLayer implements SessionLayer { + public static final NullSessionLayer INSTANCE = new NullSessionLayer(); + + private NullSessionLayer() { + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/NullTransportLayer.java b/src/main/java/li/cil/oc2/common/inet/NullTransportLayer.java new file mode 100644 index 000000000..f6e7c237d --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/NullTransportLayer.java @@ -0,0 +1,10 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.TransportLayer; + +public final class NullTransportLayer implements TransportLayer { + public static final NullTransportLayer INSTANCE = new NullTransportLayer(); + + private NullTransportLayer() { + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/SessionBase.java b/src/main/java/li/cil/oc2/common/inet/SessionBase.java new file mode 100644 index 000000000..ebc401046 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/SessionBase.java @@ -0,0 +1,79 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.Session; +import li.cil.oc2.common.Config; + +import javax.annotation.Nullable; +import java.net.InetSocketAddress; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicLong; + +public abstract class SessionBase implements Session { + private static final AtomicLong idGenerator = new AtomicLong(); + + private final long id = idGenerator.getAndIncrement(); + private final InetSocketAddress destination; + private States state; + private Instant expireTime; + private Object userdata; + + public SessionBase(final int ipAddress, final short port) { + destination = new InetSocketAddress(InetUtils.toJavaInetAddress(ipAddress), Short.toUnsignedInt(port)); + state = States.NEW; + } + + @Override + public long getId() { + return id; + } + + @Override + public void close() { + switch (state) { + case NEW: + state = States.REJECT; + return; + case ESTABLISHED: + state = States.FINISH; + return; + default: + throw new IllegalStateException(); + } + } + + @Override + public States getState() { + return state; + } + + public void setState(final States state) { + this.state = state; + } + + public void updateExpireTime() { + expireTime = Instant.now().plusMillis(Config.defaultSessionLifetimeMs); + } + + @Nullable + public Instant getExpireTime() { + return expireTime; + } + + @Nullable + @Override + public Object getUserdata() { + return this.userdata; + } + + @Override + public void setUserdata(final Object userdata) { + this.userdata = userdata; + } + + @Override + public InetSocketAddress getDestination() { + return destination; + } + + public abstract SessionDiscriminator getDiscriminator(); +} diff --git a/src/main/java/li/cil/oc2/common/inet/SessionDiscriminator.java b/src/main/java/li/cil/oc2/common/inet/SessionDiscriminator.java new file mode 100644 index 000000000..9a6021ca8 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/SessionDiscriminator.java @@ -0,0 +1,4 @@ +package li.cil.oc2.common.inet; + +interface SessionDiscriminator { +} diff --git a/src/main/java/li/cil/oc2/common/inet/SessionOperator.java b/src/main/java/li/cil/oc2/common/inet/SessionOperator.java new file mode 100644 index 000000000..7ab3b8882 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/SessionOperator.java @@ -0,0 +1,19 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.Session; + +import javax.annotation.Nullable; +import java.nio.ByteBuffer; + +public interface SessionOperator extends Session { + @Nullable + byte[] nextReceive(); + + void nextSent(final byte[] data); + + default void nextSent(final ByteBuffer data) { + final byte[] bytes = new byte[data.remaining()]; + data.get(bytes); + nextSent(bytes); + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/SocketSessionDiscriminator.java b/src/main/java/li/cil/oc2/common/inet/SocketSessionDiscriminator.java new file mode 100644 index 000000000..2a9967cc8 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/SocketSessionDiscriminator.java @@ -0,0 +1,64 @@ +package li.cil.oc2.common.inet; + +import java.util.Objects; + +public abstract class SocketSessionDiscriminator implements SessionDiscriminator { + private final int srcIpAddress; + private final short srcPort; + private final int dstIpAddress; + private final short dstPort; + + public SocketSessionDiscriminator( + final int srcIpAddress, + final short srcPort, + final int dstIpAddress, + final short dstPort + ) { + this.srcIpAddress = srcIpAddress; + this.srcPort = srcPort; + this.dstIpAddress = dstIpAddress; + this.dstPort = dstPort; + } + + public int getSrcIpAddress() { + return srcIpAddress; + } + + public short getSrcPort() { + return srcPort; + } + + public int getDstIpAddress() { + return dstIpAddress; + } + + public short getDstPort() { + return dstPort; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SocketSessionDiscriminator that = (SocketSessionDiscriminator) o; + return srcIpAddress == that.srcIpAddress + && srcPort == that.srcPort + && dstIpAddress == that.dstIpAddress + && dstPort == that.dstPort; + } + + @Override + public int hashCode() { + return Objects.hash(getClass(), srcIpAddress, srcPort, dstIpAddress, dstPort); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder("UDP("); + InetUtils.socketAddressToString(builder, srcIpAddress, srcPort); + builder.append("<->"); + InetUtils.socketAddressToString(builder, dstIpAddress, dstPort); + builder.append(')'); + return builder.toString(); + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/StreamSessionDiscriminator.java b/src/main/java/li/cil/oc2/common/inet/StreamSessionDiscriminator.java new file mode 100644 index 000000000..cc6b3437f --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/StreamSessionDiscriminator.java @@ -0,0 +1,12 @@ +package li.cil.oc2.common.inet; + +public class StreamSessionDiscriminator extends SocketSessionDiscriminator { + public StreamSessionDiscriminator( + final int srcIpAddress, + final short srcPort, + final int dstIpAddress, + final short dstPort + ) { + super(srcIpAddress, srcPort, dstIpAddress, dstPort); + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/StreamSessionImpl.java b/src/main/java/li/cil/oc2/common/inet/StreamSessionImpl.java new file mode 100644 index 000000000..40b6ae98b --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/StreamSessionImpl.java @@ -0,0 +1,17 @@ +package li.cil.oc2.common.inet; + +import li.cil.oc2.api.inet.StreamSession; + +public class StreamSessionImpl extends SessionBase implements StreamSession { + private final StreamSessionDiscriminator discriminator; + + public StreamSessionImpl(final int ipAddress, final short port, final StreamSessionDiscriminator discriminator) { + super(ipAddress, port); + this.discriminator = discriminator; + } + + @Override + public StreamSessionDiscriminator getDiscriminator() { + return discriminator; + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/TcpHeader.java b/src/main/java/li/cil/oc2/common/inet/TcpHeader.java new file mode 100644 index 000000000..bc8651458 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/TcpHeader.java @@ -0,0 +1,71 @@ +package li.cil.oc2.common.inet; + +import java.nio.ByteBuffer; + +public class TcpHeader { + private static final int MIN_HEADER_SIZE = 20; + + private static final byte OPTION_END = 0; + private static final byte OPTION_NOOP = 1; + private static final byte OPTION_MAX_SEGMENT_SIZE = 2; + + //////////////////////////////////////////////////////////////////////////// + + public int sequenceNumber, acknowledgmentNumber; + public boolean urg, ack, psh, rst, syn, fin; // flags + public int window; + public int urgentPointer; + + // Options + public int maxSegmentSize; + + //////////////////////////////////////////////////////////////////////////// + + public boolean read(final ByteBuffer data) { + if (data.remaining() < MIN_HEADER_SIZE) { + return false; + } + final int position = data.position(); + sequenceNumber = data.getInt(); + acknowledgmentNumber = data.getInt(); + final int dataOffset = position + ((data.get() >>> 2) & 0x3C); + final int flags = Byte.toUnsignedInt(data.get()); + urg = ((flags >>> 5) & 1) == 1; + ack = ((flags >>> 4) & 1) == 1; + psh = ((flags >>> 3) & 1) == 1; + rst = ((flags >>> 2) & 1) == 1; + syn = ((flags >>> 1) & 1) == 1; + fin = (flags & 1) == 1; + window = Short.toUnsignedInt(data.getShort()); + data.getShort(); // checksum + urgentPointer = Short.toUnsignedInt(data.getShort()); + + maxSegmentSize = -1; + + while (dataOffset > data.position()) { + final byte type = data.get(); + switch (type) { + case OPTION_END: + data.position(dataOffset); + return true; + case OPTION_NOOP: + continue; + default: + break; + } + final int size = Byte.toUnsignedInt(data.get()); + if (type == OPTION_MAX_SEGMENT_SIZE) { + if (size != 2) { + data.position(position); + return false; + } + maxSegmentSize = Short.toUnsignedInt(data.getShort()); + } else { + // Skip unknown option + data.position(data.position() + size - 2); + } + } + data.position(dataOffset); + return true; + } +} diff --git a/src/main/java/li/cil/oc2/common/inet/package-info.java b/src/main/java/li/cil/oc2/common/inet/package-info.java new file mode 100644 index 000000000..276f28cfd --- /dev/null +++ b/src/main/java/li/cil/oc2/common/inet/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package li.cil.oc2.common.inet; + +import mcp.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file 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 2b4fb06df..14cf05ab8 100644 --- a/src/main/java/li/cil/oc2/common/item/Items.java +++ b/src/main/java/li/cil/oc2/common/item/Items.java @@ -64,6 +64,7 @@ public final class Items { public static final RegistryObject REDSTONE_INTERFACE_CARD = register("redstone_interface_card"); public static final RegistryObject NETWORK_INTERFACE_CARD = register("network_interface_card"); + public static final RegistryObject INTERNET_CARD = register("internet_card"); public static final RegistryObject FILE_IMPORT_EXPORT_CARD = register("file_import_export_card"); public static final RegistryObject SOUND_CARD = register("sound_card"); diff --git a/src/main/java/li/cil/oc2/common/util/IntegerSpace.java b/src/main/java/li/cil/oc2/common/util/IntegerSpace.java new file mode 100644 index 000000000..1d5192035 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/util/IntegerSpace.java @@ -0,0 +1,124 @@ +package li.cil.oc2.common.util; + +import java.util.Iterator; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +/** + * A set of integers that is more effective with ranges of integers. + */ +public class IntegerSpace { + private final NavigableMap ranges = new TreeMap<>(); + + public final boolean put(final int element) { + return put(element, element); + } + + public final boolean put(final int begin, final int end) { + if (end < begin) { + return put(end, begin); + } + + ranges.subMap(begin, false, end, false).entrySet() + .removeIf(range -> range.getKey() > begin && range.getValue() < end); + + final Map.Entry floorBegin = ranges.floorEntry(begin); + final Map.Entry higherEnd = ranges.ceilingEntry(end); + + if (floorBegin != null + && floorBegin.getKey() <= begin && floorBegin.getValue() >= end) { + // Already exists in the space + // [---------] + // [---------] + // [---] + // [---------] + return false; + } else if (floorBegin != null && higherEnd != null + && floorBegin.getKey() <= begin && floorBegin.getValue() + 1 >= begin + && higherEnd.getKey() - 1 <= end && higherEnd.getValue() >= end) { + // Remove whitespace between 2 ranges + // [---------] [----------] + // [------] + // [------------] + // [---------] + // [---------] + ranges.entrySet().remove(higherEnd); + ranges.put(floorBegin.getKey(), higherEnd.getValue()); + } else if (higherEnd != null + && higherEnd.getKey() - 1 <= end && higherEnd.getValue() >= end) { + // Change higher range start position + // [---------] [---------] + // [----] + // [---------] + ranges.entrySet().remove(higherEnd); + ranges.put(begin, higherEnd.getValue()); + } else if (floorBegin != null + && floorBegin.getKey() <= begin && floorBegin.getValue() + 1 >= begin) { + // New range after some other range + // [---------] + // [--] + // [------] + ranges.put(floorBegin.getKey(), end); + } else { + // New range in empty space or new range before all others + // [---------] + // [-----] + ranges.put(begin, end); + } + return true; + } + + public final boolean contains(final int element) { + final Map.Entry floorRange = ranges.floorEntry(element); + return floorRange != null && element >= floorRange.getKey() && element <= floorRange.getValue(); + } + + public final boolean isEmpty() { + return ranges.isEmpty(); + } + + public final int rangeCount() { + return ranges.size(); + } + + public final int count() { + return ranges.entrySet().stream() + .map(range -> range.getValue() - range.getKey() + 1) + .reduce(0, Integer::sum); + } + + protected void elementToString(final StringBuilder builder, final int element) { + builder.append(element); + } + + private void appendRangeToString(final StringBuilder builder, final Map.Entry range) { + final int begin = range.getKey(); + final int end = range.getValue(); + elementToString(builder, begin); + if (begin != end) { + builder.append('-'); + elementToString(builder, range.getValue()); + } + } + + @Override + public String toString() { + final Iterator> iterator = ranges.entrySet().iterator(); + if (iterator.hasNext()) { + final StringBuilder builder = new StringBuilder(); + builder.append('['); + final Map.Entry first = iterator.next(); + appendRangeToString(builder, first); + while (iterator.hasNext()) { + builder.append(", "); + final Map.Entry range = iterator.next(); + appendRangeToString(builder, range); + } + builder.append(']'); + return builder.toString(); + } else { + return "[]"; + } + } +} diff --git a/src/main/resources/assets/oc2/models/item/internet_card.json b/src/main/resources/assets/oc2/models/item/internet_card.json new file mode 100644 index 000000000..a669a237b --- /dev/null +++ b/src/main/resources/assets/oc2/models/item/internet_card.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "oc2:item/internet_card" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/oc2/textures/item/internet_card.png b/src/main/resources/assets/oc2/textures/item/internet_card.png new file mode 100644 index 000000000..cda92cf27 Binary files /dev/null and b/src/main/resources/assets/oc2/textures/item/internet_card.png differ diff --git a/src/main/resources/assets/oc2/textures/item/internet_card.png.mcmeta b/src/main/resources/assets/oc2/textures/item/internet_card.png.mcmeta new file mode 100644 index 000000000..3b25383d3 --- /dev/null +++ b/src/main/resources/assets/oc2/textures/item/internet_card.png.mcmeta @@ -0,0 +1,47 @@ +{ + "animation": { + "frametime": 1, + "frames": [ + { + "index": 0, + "time": 2 + }, + { + "index": 1, + "time": 7 + }, + { + "index": 0, + "time": 5 + }, + { + "index": 1, + "time": 4 + }, + { + "index": 0, + "time": 7 + }, + { + "index": 1, + "time": 2 + }, + { + "index": 0, + "time": 8 + }, + { + "index": 1, + "time": 9 + }, + { + "index": 0, + "time": 6 + }, + { + "index": 1, + "time": 4 + } + ] + } +} \ No newline at end of file diff --git a/src/test/java/li/cil/oc2/common/inet/Ipv4SpaceTest.java b/src/test/java/li/cil/oc2/common/inet/Ipv4SpaceTest.java new file mode 100644 index 000000000..8d8d4dba9 --- /dev/null +++ b/src/test/java/li/cil/oc2/common/inet/Ipv4SpaceTest.java @@ -0,0 +1,38 @@ +package li.cil.oc2.common.inet; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class Ipv4SpaceTest { + @Test + public void someRangesAndSubnetsTest() { + final Ipv4Space ipv4Space = new Ipv4Space(Ipv4Space.Modes.ALLOWLIST); + ipv4Space.put("127.0.1.1"); + ipv4Space.put("one.one.one.one"); + ipv4Space.put("127.0.0.1/24"); + ipv4Space.put("10.0.0.0/30"); + ipv4Space.put("172.17.0.0/16"); + ipv4Space.put("192.168.30.1-192.168.30.20"); + final String expected = "[" + + "172.17.0.0-172.17.255.255, " + + "192.168.30.1-192.168.30.20, " + + "1.0.0.1, 1.1.1.1, 10.0.0.0-10.0.0.3, " + + "127.0.0.0-127.0.0.255, 127.0.1.1" + + "]"; + assertEquals(expected, ipv4Space.toString()); + + assertTrue(ipv4Space.isAllowed(InetUtils.parseIpv4Address("127.0.0.1"))); + assertTrue(ipv4Space.isAllowed(InetUtils.parseIpv4Address("1.0.0.1"))); + assertTrue(ipv4Space.isAllowed(InetUtils.parseIpv4Address("192.168.30.10"))); + assertFalse(ipv4Space.isAllowed(InetUtils.parseIpv4Address("192.168.30.21"))); + } + + @Test + public void computeIpSpaceTest() { + final Ipv4Space space = InetUtils.computeIpSpace("127.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4", " "); + assertEquals("[172.16.0.0-172.31.255.255, 192.168.0.0-192.168.255.255, 224.0.0.0-239.255.255.255, 10.0.0.0-10.255.255.255, 100.64.0.0-100.127.255.255, 127.0.0.0-127.255.255.255]", space.toString()); + assertFalse(space.isAllowed(InetUtils.parseIpv4Address("192.168.1.1"))); + assertTrue(space.isAllowed(InetUtils.parseIpv4Address("1.1.1.1"))); + } +} diff --git a/src/test/java/li/cil/oc2/common/util/IntegerSpaceTest.java b/src/test/java/li/cil/oc2/common/util/IntegerSpaceTest.java new file mode 100644 index 000000000..aeac50760 --- /dev/null +++ b/src/test/java/li/cil/oc2/common/util/IntegerSpaceTest.java @@ -0,0 +1,112 @@ +package li.cil.oc2.common.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class IntegerSpaceTest { + @Test + void integerSpaceTest() { + final IntegerSpace space = new IntegerSpace(); + try { + assertEquals(0, space.count()); + assertFalse(space.contains(30)); + + assertTrue(space.put(30)); // [30] + assertFalse(space.contains(29)); + assertTrue(space.contains(30)); + assertFalse(space.contains(31)); + assertEquals(1, space.rangeCount()); + assertEquals(1, space.count()); + + assertTrue(space.put(31)); // [30-31] + assertEquals(1, space.rangeCount()); + assertEquals(2, space.count()); + assertFalse(space.contains(29)); + assertTrue(space.contains(31)); + assertFalse(space.contains(32)); + + assertTrue(space.put(32)); // [30-32] + assertEquals(1, space.rangeCount()); + assertEquals(3, space.count()); + assertFalse(space.contains(29)); + assertTrue(space.contains(30)); + assertTrue(space.contains(31)); + assertTrue(space.contains(32)); + assertFalse(space.contains(33)); + + assertTrue(space.put(29)); // [29-32] + assertEquals(1, space.rangeCount()); + assertEquals(4, space.count()); + assertFalse(space.contains(28)); + assertTrue(space.contains(29)); + assertTrue(space.contains(30)); + assertTrue(space.contains(31)); + assertTrue(space.contains(32)); + assertFalse(space.contains(33)); + + assertFalse(space.put(29)); + assertFalse(space.put(30)); + assertFalse(space.put(31)); + assertFalse(space.put(32)); + + assertTrue(space.put(34)); // [29-32] [34] + assertFalse(space.contains(33)); + assertEquals(2, space.rangeCount()); + assertEquals(5, space.count()); + assertFalse(space.contains(33)); + assertTrue(space.contains(34)); + + assertTrue(space.put(33)); // [29-34] + assertEquals(1, space.rangeCount()); + assertEquals(6, space.count()); + assertTrue(space.contains(33)); + + assertTrue(space.put(38)); // [29-34] [38] + assertEquals(2, space.rangeCount()); + assertEquals(7, space.count()); + assertTrue(space.contains(38)); + + assertTrue(space.put(37)); // [29-34] [37-38] + assertEquals(2, space.rangeCount()); + assertEquals(8, space.count()); + assertTrue(space.contains(37)); + assertFalse(space.contains(36)); + assertFalse(space.contains(35)); + + assertTrue(space.put(35)); // [29-35] [37-38] + assertEquals(2, space.rangeCount()); + assertEquals(9, space.count()); + assertTrue(space.contains(37)); + assertFalse(space.contains(36)); + assertTrue(space.contains(35)); + + assertTrue(space.put(27)); // [27] [29-35] [37-38] + assertEquals(3, space.rangeCount()); + assertEquals(10, space.count()); + assertTrue(space.contains(27)); + assertFalse(space.contains(28)); + assertTrue(space.contains(29)); + + assertTrue(space.put(31, 37)); // [27] [29-38] + assertEquals(2, space.rangeCount()); + assertEquals(11, space.count()); + assertTrue(space.contains(27)); + assertFalse(space.contains(28)); + for (int i = 29; i <= 38; ++i) { + assertTrue(space.contains(i), Integer.toString(i)); + } + + assertTrue(space.put(33, 39)); // [27] [29-39] + assertEquals(2, space.rangeCount()); + assertEquals(12, space.count()); + assertEquals("[27, 29-39]", space.toString()); + + assertTrue(space.put(23, 26)); + assertEquals("[23-27, 29-39]", space.toString()); + } catch (final AssertionError e) { + System.out.println("Space state: " + space); + throw e; + } + } +}