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:
+ *
+ *
+ * - {@link LinkLocalLayerInternetProvider}
+ * - {@link NetworkLayerInternetProvider}
+ * - {@link TransportLayerInternetProvider}
+ * - {@link SessionLayerInternetProvider}
+ *
+ *
+ * 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;
+ }
+ }
+}