CVE-2023-37261: Server-Side Request Forgery (SSRF) (CWE-918)
value: OpenComputers is a Minecraft mod that adds programmable computers and robots to the game. This issue affects every version of OpenComputers with the Internet Card feature enabled; that is, OpenComputers 1.2.0 until 1.8.3 in their most common, default configurations. If the OpenComputers mod is installed as part of a Minecraft server hosted on a popular cloud hosting provider, such as AWS, GCP and Azure, those metadata services' API endpoints are not forbidden (aka "blacklisted") by default. As such, any player can gain access to sensitive information exposed via those metadata servers, potentially allowing them to pivot or privilege escalate into the hosting provider. In addition, IPv6 addresses are not correctly filtered at all, allowing broader access into the local IPv6 network. This can allow a player on a server using an OpenComputers computer to access parts of the private IPv4 address space, as well as the whole IPv6 address space, in order to retrieve sensitive information.

OpenComputers v1.8.3 for Minecraft 1.7.10 and 1.12.2 contains a patch for this issue. Some workarounds are also available. One may disable the Internet Card feature completely. If using OpenComputers 1.3.0 or above, using the allow list (`opencomputers.internet.whitelist` option) will prohibit connections to any IP addresses and/or domains not listed; or one may add entries to the block list (`opencomputers.internet.blacklist` option). More information about mitigations is available in the GitHub Security Advisory.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 8.8
Published: 2023-07-07T21:15Z
Repository: https://github.com/MightyPirates/OpenComputers
Language: Scala
Files changed: changelog.md, src/main/java/com/typesafe/config/impl/OpenComputersConfigCommentManipulationHook.java, src/main/java/li/cil/oc/util/InetAddressRange.java, src/main/resources/application.conf, src/main/scala/li/cil/oc/OpenComputers.scala, src/main/scala/li/cil/oc/Settings.scala, src/main/scala/li/cil/oc/server/component/InternetCard.scala, src/main/scala/li/cil/oc/util/InternetFilteringRule.scala, src/main/scala/li/cil/oc/util/ThreadPoolFactory.scala, src/test/scala/InternetFilteringRuleTest.scala
Commit date: 2023-07-07 17:45:01 +0200
Commit message: Rework Internet Card filtering system.

Diff: diff --git a/changelog.md b/changelog.md
index 24fd723e0..3f8a41a76 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,7 +1,11 @@
 ## Fixes/improvements
 
+* Reworked Internet Card filtering rules.
+  * Implemented a new, more powerful system and improved default configuration.
+  * Internet Card rules are now stored in the "internet.filteringRules" configuration key.
+  * The old keys ("internet.whitelist", "internet.blacklist") are no longer used; an automatic migration is done upon upgrading the mod.
 * [#3635] ArrayIndexOutOfBoundsException when using servers with 3 network cards
 * [#3634] Internet card selector update logic erroneously drops non-ready keys
 
 ## List of contributors
 
diff --git a/src/main/java/com/typesafe/config/impl/OpenComputersConfigCommentManipulationHook.java b/src/main/java/com/typesafe/config/impl/OpenComputersConfigCommentManipulationHook.java
new file mode 100644
index 000000000..67666a26b
--- /dev/null
+++ b/src/main/java/com/typesafe/config/impl/OpenComputersConfigCommentManipulationHook.java
@@ -0,0 +1,27 @@
+package com.typesafe.config.impl;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigValue;
+import org.luaj.vm2.ast.Str;
+
+import java.util.List;
+
+public final class OpenComputersConfigCommentManipulationHook {
+    private OpenComputersConfigCommentManipulationHook() {
+
+    }
+
+    public static Config setComments(Config config, String path, List<String> comments) {
+        return config.withValue(path, setComments(config.getValue(path), comments));
+    }
+
+    public static ConfigValue setComments(ConfigValue value, List<String> comments) {
+        if (value.origin() instanceof SimpleConfigOrigin && value instanceof AbstractConfigValue) {
+            return ((AbstractConfigValue) value).withOrigin(
+                ((SimpleConfigOrigin) value.origin()).setComments(comments)
+            );
+        } else {
+            return value;
+        }
+    }
+}
diff --git a/src/main/java/li/cil/oc/util/InetAddressRange.java b/src/main/java/li/cil/oc/util/InetAddressRange.java
new file mode 100644
index 000000000..3f1898cfe
--- /dev/null
+++ b/src/main/java/li/cil/oc/util/InetAddressRange.java
@@ -0,0 +1,64 @@
+
+package li.cil.oc.util;
+
+import com.google.common.net.InetAddresses;
+
+import java.net.InetAddress;
+
+// Originally by SquidDev
+public final class InetAddressRange {
+    private final byte[] min;
+    private final byte[] max;
+
+    InetAddressRange(byte[] min, byte[] max) {
+        this.min = min;
+        this.max = max;
+    }
+
+    public boolean matches(InetAddress address) {
+        byte[] entry = address.getAddress();
+        if (entry.length != min.length) return false;
+
+        for (int i = 0; i < entry.length; i++) {
+            int value = 0xFF & entry[i];
+            if (value < (0xFF & min[i]) || value > (0xFF & max[i])) return false;
+        }
+
+        return true;
+    }
+
+    public static InetAddressRange parse(String addressStr, String prefixSizeStr) {
+        int prefixSize;
+        try {
+            prefixSize = Integer.parseInt(prefixSizeStr);
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(String.format("Malformed address range entry '%s': Cannot extract size of CIDR mask from '%s'.",
+                    addressStr + '/' + prefixSizeStr, prefixSizeStr));
+        }
+
+        InetAddress address;
+        try {
+            address = InetAddresses.forString(addressStr);
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException(String.format("Malformed address range entry '%s': Cannot extract IP address from '%s'.",
+                    addressStr + '/' + prefixSizeStr, addressStr));
+        }
+
+        // Mask the bytes of the IP address.
+        byte[] minBytes = address.getAddress(), maxBytes = address.getAddress();
+        int size = prefixSize;
+        for (int i = 0; i < minBytes.length; i++) {
+            if (size <= 0) {
+                minBytes[i] = (byte) 0;
+                maxBytes[i] = (byte) 0xFF;
+            } else if (size < 8) {
+                minBytes[i] = (byte) (minBytes[i] & 0xFF << (8 - size));
+                maxBytes[i] = (byte) (maxBytes[i] | ~(0xFF << (8 - size)));
+            }
+
+            size -= 8;
+        }
+
+        return new InetAddressRange(minBytes, maxBytes);
+    }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf
index b82383639..b55cd3c49 100644
--- a/src/main/resources/application.conf
+++ b/src/main/resources/application.conf
@@ -961,41 +961,48 @@ opencomputers {
 
     # Whether to allow TCP connections via internet cards. When enabled,
     # the `connect` method on internet card components becomes available.
     enableTcp: true
 
-    # This is a list of forbidden domain names. If an HTTP request is made
-    # or a socket connection is opened the target address will be compared
-    # to the addresses / address ranges in this list. It it is present in this
-    # list, the request will be denied.
-    # Entries are either domain names (www.example.com) or IP addresses in
-    # string format (10.0.0.3), optionally in CIDR notation to make it easier
-    # to define address ranges (1.0.0.0/8). Domains are resolved to their
-    # actual IP once on startup, future requests are resolved and compared
-    # to the resolved addresses.
-    # By default all local addresses are blocked. This is only meant as a
+    # This is a list of filtering rules. For any HTTP request or TCP socket
+    # connection, the target address will be processed by each rule, starting
+    # from first to last. The first matching rule will be applied; if no rule
+    # contains a match, the request will be denied.
+    # Two types of rules are currently supported: "allow", which allows an
+    # address to be accessed, and "deny", which forbids such access.
+    # Rules can be suffixed with additional filters to limit their scope:
+    # - all: apply to all addresses
+    # - default: apply built-in allow/deny rules; these may not be up to date,
+    #   so one should primarily rely on them as a fallback
+    # - private: apply to all private addresses
+    # - bogon: apply to all known bogon addresses
+    # - ipv4: apply to all IPv4 addresses
+    # - ipv6: apply to all IPv6 addresses
+    # - ipv4-embedded-ipv6: apply to all IPv4 addresses embedded in IPv6
+    #   addresses
+    # - ip:[address]: apply to this IP address in string format (10.0.0.3).
+    #   CIDR notation is supported and allows defining address ranges
+    #   (1.0.0.0/8).
+    # - domain:[domain]: apply to this domain. Domains are resolved to their
+    #   actual IP only once (on startup), future requests are resolved and
+    #   compared to the resolved addresses. Wildcards are not supported.
+    # The "removeme" rule does not have any use, but is instead present to
+    # detect whether to emit a warning on dedicated server configurations.
+    # Modpack authors are asked not to remove this rule; server administrators
+    # are free to remove it once the filtering rules have been adjusted.
+    # By default all private addresses are blocked. This is only meant as a
     # thin layer of security, to avoid average users hosting a game on their
     # local machine having players access services in their local network.
     # Server hosters are expected to configure their network outside of the
     # mod's context in an appropriate manner, e.g. using a system firewall.
-    blacklist: [
-      "127.0.0.0/8"
-      "0.0.0.0/8"
-      "10.0.0.0/8"
-      "192.168.0.0/16"
-      "172.16.0.0/12"
+    filteringRules: [
+      "removeme",
+      "deny private",
+      "deny bogon",
+      "allow default"
     ]
 
-    # This is a list of allowed domain names. Requests may only be made
-    # to addresses that are present in this list. If this list is empty,
-    # requests may be made to all addresses not forbidden. Note that the
-    # blacklist is always applied, so if an entry is present in both the
-    # whitelist and the blacklist, the blacklist will win.
-    # Entries are of the same format as in the blacklist. Examples:
-    # "gist.github.com", "www.pastebin.com"
-    whitelist: []
-
     # The time in seconds to wait for a response to a request before timing
     # out and returning an error message. If this is zero (the default) the
     # request will never time out.
     requestTimeout: 0
 
diff --git a/src/main/scala/li/cil/oc/OpenComputers.scala b/src/main/scala/li/cil/oc/OpenComputers.scala
index fa57c1138..2f30be48f 100644
--- a/src/main/scala/li/cil/oc/OpenComputers.scala
+++ b/src/main/scala/li/cil/oc/OpenComputers.scala
@@ -58,10 +58,36 @@ object OpenComputers {
 
   @EventHandler
   def serverStart(e: FMLServerStartingEvent): Unit = {
     CommandHandler.register(e)
     ThreadPoolFactory.safePools.foreach(_.newThreadPool())
+
+    if (Settings.get.internetAccessConfigured()) {
+      if (Settings.get.internetFilteringRulesInvalid()) {
+        OpenComputers.log.warn("####################################################")
+        OpenComputers.log.warn("#                                                  #")
+        OpenComputers.log.warn("#  Could not parse Internet Card filtering rules!  #")
+        OpenComputers.log.warn("#  Review the server log and adjust the filtering  #")
+        OpenComputers.log.warn("#  list to ensure it is appropriately configured.  #")
+        OpenComputers.log.warn("#   (config/OpenComputers.cfg => filteringRules)   #")
+        OpenComputers.log.warn("# Internet access has been automatically disabled. #")
+        OpenComputers.log.warn("#                                                  #")
+        OpenComputers.log.warn("####################################################")
+      } else if (!Settings.get.internetFilteringRulesObserved && e.getServer.isDedicatedServer) {
+        OpenComputers.log.warn("####################################################")
+        OpenComputers.log.warn("#                                                  #")
+        OpenComputers.log.warn("#    It appears that you're running a dedicated    #")
+        OpenComputers.log.warn("#  server with OpenComputers installed! Make sure  #")
+        OpenComputers.log.warn("#  to review the Internet Card address filtering   #")
+        OpenComputers.log.warn("#  list to ensure it is appropriately configured.  #")
+        OpenComputers.log.warn("#   (config/OpenComputers.cfg => filteringRules)   #")
+        OpenComputers.log.warn("#                                                  #")
+        OpenComputers.log.warn("####################################################")
+      } else {
+        OpenComputers.log.info(f"Successfully applied ${Settings.get.internetFilteringRules.length} Internet Card filtering rules.")
+      }
+    }
   }
 
   @EventHandler
   def serverStop(e: FMLServerStoppedEvent): Unit = {
     ThreadPoolFactory.safePools.foreach(_.waitForCompletion())
diff --git a/src/main/scala/li/cil/oc/Settings.scala b/src/main/scala/li/cil/oc/Settings.scala
index 295c22afe..ee69aa7d5 100644
--- a/src/main/scala/li/cil/oc/Settings.scala
+++ b/src/main/scala/li/cil/oc/Settings.scala
@@ -1,30 +1,30 @@
 package li.cil.oc
 
-import java.io._
-import java.net.Inet4Address
-import java.net.InetAddress
-import java.nio.charset.StandardCharsets
-import java.security.SecureRandom
-import java.util.UUID
 import com.google.common.net.InetAddresses
 import com.mojang.authlib.GameProfile
 import com.typesafe.config._
+import com.typesafe.config.impl.OpenComputersConfigCommentManipulationHook
 import cpw.mods.fml.common.Loader
 import cpw.mods.fml.common.versioning.{DefaultArtifactVersion, VersionRange}
 import li.cil.oc.Settings.DebugCardAccess
 import li.cil.oc.common.Tier
-import li.cil.oc.integration.Mods
 import li.cil.oc.server.component.DebugCard
 import li.cil.oc.server.component.DebugCard.AccessContext
+import li.cil.oc.util.{InetAddressRange, InternetFilteringRule}
 import org.apache.commons.codec.binary.Hex
 import org.apache.commons.lang3.StringEscapeUtils
 
+import java.io._
+import java.net.{Inet4Address, Inet6Address, InetAddress}
+import java.nio.charset.StandardCharsets
+import java.security.SecureRandom
+import java.util.UUID
+import scala.collection.JavaConverters._
 import scala.collection.convert.WrapAsScala._
 import scala.collection.mutable
-import scala.io.Codec
-import scala.io.Source
+import scala.io.{Codec, Source}
 import scala.util.matching.Regex
 
 class Settings(val config: Config) {
   // ----------------------------------------------------------------------- //
   // client
@@ -297,12 +297,15 @@ class Settings(val config: Config) {
   // ----------------------------------------------------------------------- //
   // internet
   val httpEnabled = config.getBoolean("internet.enableHttp")
   val httpHeadersEnabled = config.getBoolean("internet.enableHttpHeaders")
   val tcpEnabled = config.getBoolean("internet.enableTcp")
-  val httpHostBlacklist = Array(config.getStringList("internet.blacklist").map(new Settings.AddressValidator(_)): _*)
-  val httpHostWhitelist = Array(config.getStringList("internet.whitelist").map(new Settings.AddressValidator(_)): _*)
+  val internetFilteringRules = Array(config.getStringList("internet.filteringRules")
+    .filter(p => !p.equals("removeme"))
+    .map(new InternetFilteringRule(_)): _*)
+  val internetFilteringRulesObserved = !config.getStringList("internet.filteringRules")
+    .contains("removeme")
   val httpTimeout = (config.getInt("internet.requestTimeout") max 0) * 1000
   val maxConnections = config.getInt("internet.maxTcpConnections") max 0
   val internetThreads = config.getInt("internet.threads") max 1
 
   // ----------------------------------------------------------------------- //
@@ -482,10 +485,22 @@ class Settings(val config: Config) {
   // >= 1.8.2
   val diskActivitySoundDelay: Int = config.getInt("misc.diskActivitySoundDelay") max -1
   val maxNetworkClientPacketDistance: Double = config.getDouble("misc.maxNetworkClientPacketDistance") max 0
   val maxNetworkClientEffectPacketDistance: Double = config.getDouble("misc.maxNetworkClientEffectPacketDistance") max 0
   val maxNetworkClientSoundPacketDistance: Double = config.getDouble("misc.maxNetworkClientSoundPacketDistance") max 0
+
+  def internetFilteringRulesInvalid(): Boolean = {
+    internetFilteringRules.exists(p => p.invalid())
+  }
+
+  def internetAccessConfigured(): Boolean = {
+    httpEnabled || tcpEnabled
+  }
+
+  def internetAccessAllowed(): Boolean = {
+    internetAccessConfigured() && !internetFilteringRulesInvalid()
+  }
 }
 
 object Settings {
   val resourceDomain = "opencomputers"
   val namespace = "oc:"
@@ -494,10 +509,15 @@ object Settings {
   val screenResolutionsByTier: Array[(Int, Int)] = Array((50, 16), (80, 25), (160, 50))
   val screenDepthsByTier: Array[api.internal.TextBuffer.ColorDepth] = Array(api.internal.TextBuffer.ColorDepth.OneBit, api.internal.TextBuffer.ColorDepth.FourBit, api.internal.TextBuffer.ColorDepth.EightBit)
   val deviceComplexityByTier: Array[Int] = Array(12, 24, 32, 9001)
   var rTreeDebugRenderer = false
   var blockRenderId: Int = -1
+  private val forbiddenConfigLists: List[String] = List(
+    /* 1.8.3+ filtering rules migration */
+    "internet.blacklist", "internet.whitelist"
+  )
+  private val prefix = "opencomputers."
 
   def basicScreenPixels: Int = screenResolutionsByTier(0)._1 * screenResolutionsByTier(0)._2
 
   private var settings: Settings = _
 
@@ -529,10 +549,17 @@ object Settings {
             throw new RuntimeException("Error parsing configuration file. To restore defaults, delete '" + file.getName + "' and restart the game.", e)
           }
           settings = new Settings(defaults.getConfig("opencomputers"))
           defaults
       }
+    for (key <- forbiddenConfigLists) {
+      if (config.hasPath(prefix + key)) {
+        if (!config.getStringList(prefix + key).isEmpty) {
+          throw new RuntimeException("Error parsing configuration file: removed configuration option '" + key + "' is not empty. This option should no longer be used.")
+        }
+      }
+    }
     try {
       val renderSettings = ConfigRenderOptions.defaults.setJson(false).setOriginComments(false)
       val nl = sys.props("line.separator")
       val nle = StringEscapeUtils.escapeJava(nl)
       val out = new PrintWriter(file)
@@ -579,63 +606,87 @@ object Settings {
     // Upgrading to version 1.8.0, changed meaning of limitFlightHeight value,
     VersionRange.createFromVersionSpec("[0.0, 1.8.0)") -> Array(
       "computer.robot.limitFlightHeight"
     )
   )
+  private val fileringRulesPatchVersion = VersionRange.createFromVersionSpec("[0.0, 1.8.3)")
 
   // Checks the config version (i.e. the version of the mod the config was
   // created by) against the current version to see if some hard changes
   // were made. If so, the new default values are copied over.
   private def patchConfig(config: Config, defaults: Config) = {
     val mod = Loader.instance.activeModContainer
-    val prefix = "opencomputers."
     val configVersion = new DefaultArtifactVersion(if (config.hasPath(prefix + "version")) config.getString(prefix + "version") else "0.0.0")
     var patched = config
     if (configVersion.compareTo(mod.getProcessedVersion) != 0) {
       OpenComputers.log.info(s"Updating config from version '${configVersion.getVersionString}' to '${defaults.getString(prefix + "version")}'.")
       patched = patched.withValue(prefix + "version", defaults.getValue(prefix + "version"))
       for ((version, paths) <- configPatches if version.containsVersion(configVersion)) {
         for (path <- paths) {
           val fullPath = prefix + path
-          OpenComputers.log.info(s"Updating setting '$fullPath'. ")
+          OpenComputers.log.info(s"=> Updating setting '$fullPath'. ")
           if (defaults.hasPath(fullPath)) {
             patched = patched.withValue(fullPath, defaults.getValue(fullPath))
           }
           else {
             patched = patched.withoutPath(fullPath)
           }
         }
       }
-    }
-    patched
-  }
 
-  val cidrPattern = """(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:/(\d{1,2}))""".r
-
-  class AddressValidator(val value: String) {
-    val validator: (InetAddress, String) => Option[Boolean] = try cidrPattern.findFirstIn(value) match {
-      case Some(cidrPattern(address, prefix)) =>
-        val addr = InetAddresses.coerceToInteger(InetAddresses.forString(address))
-        val mask = 0xFFFFFFFF << (32 - prefix.toInt)
-        val min = addr & mask
-        val max = min | ~mask
-        (inetAddress: InetAddress, host: String) => Some(inetAddress match {
-          case v4: Inet4Address =>
-            val numeric = InetAddresses.coerceToInteger(v4)
-            min <= numeric && numeric <= max
-          case _ => true // Can't check IPv6 addresses so we pass them.
-        })
-      case _ =>
-        val address = InetAddress.getByName(value)
-        (inetAddress: InetAddress, host: String) => Some(host == value || inetAddress == address)
-    } catch {
-      case t: Throwable =>
-        OpenComputers.log.warn("Invalid entry in internet blacklist / whitelist: " + value, t)
-        (inetAddress: InetAddress, host: String) => None
+      // Migrate filtering rules to 1.8.3+
+      if (fileringRulesPatchVersion.containsVersion(configVersion)) {
+        OpenComputers.log.info(s"=> Migrating Internet Card filtering rules. ")
+        val cidrPattern = """(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:/(\d{1,2}))""".r
+        val httpHostWhitelist = patched.getStringList(prefix + "internet.whitelist")
+        val httpHostBlacklist = patched.getStringList(prefix + "internet.blacklist")
+        val internetFilteringRules = mutable.MutableList[String]()
+        for (blockedAddress <- httpHostBlacklist) {
+          if (cidrPattern.findFirstIn(blockedAddress).isDefined) {
+            internetFilteringRules += "deny ip:" + blockedAddress
+          } else {
+            internetFilteringRules += "deny domain:" + blockedAddress
+          }
+        }
+        for (allowedAddress <- httpHostWhitelist) {
+          if (cidrPattern.findFirstIn(allowedAddress).isDefined) {
+            internetFilteringRules += "allow ip:" + allowedAddress
+          } else {
+            internetFilteringRules += "allow domain:" + allowedAddress
+          }
+        }
+        if (!httpHostWhitelist.isEmpty) {
+          internetFilteringRules += "deny all"
+        }
+        for (defaultRule <- defaults.getStringList(prefix + "internet.filteringRules")) {
+          internetFilteringRules += defaultRule
+        }
+        var patchedRules: ConfigValue = ConfigValueFactory.fromIterable(internetFilteringRules.asJava)
+        // We need to use the private APIs here, unfortunately.
+        try {
+          for (key <- List("internet.whitelist", "internet.blacklist")) {
+            if (patched.hasPath(prefix + key)) {
+              val originalValue = patched.getValue(prefix + key)
+              var deprecatedValue: ConfigValue = ConfigValueFactory.fromIterable(new java.util.ArrayList[String](), originalValue.origin().description())
+              val comments = mutable.MutableList("No longer used! See internet.filteringRules.", "", "Previous contents:")
+              for (value <- patched.getStringList(prefix + key)) {
+                comments += "\"" + value + "\""
+              }
+              deprecatedValue = OpenComputersConfigCommentManipulationHook.setComments(deprecatedValue, comments.asJava)
+              patched = patched.withValue(prefix + key, deprecatedValue)
+            }
+          }
+          patchedRules = OpenComputersConfigCommentManipulationHook.setComments(
+            patchedRules, defaults.getValue(prefix + "internet.filteringRules").origin().comments()
+          )
+        } catch {
+          case _: Throwable => /* pass */
+        }
+        patched = patched.withValue(prefix + "internet.filteringRules", patchedRules)
+      }
     }
-
-    def apply(inetAddress: InetAddress, host: String) = validator(inetAddress, host)
+    patched
   }
 
   sealed trait DebugCardAccess {
     def checkAccess(ctx: Option[DebugCard.AccessContext]): Option[String]
   }
diff --git a/src/main/scala/li/cil/oc/server/component/InternetCard.scala b/src/main/scala/li/cil/oc/server/component/InternetCard.scala
index 2b15bf2ad..2c6268269 100644
--- a/src/main/scala/li/cil/oc/server/component/InternetCard.scala
+++ b/src/main/scala/li/cil/oc/server/component/InternetCard.scala
@@ -1,7 +1,9 @@
 package li.cil.oc.server.component
 
+import com.google.common.net.InetAddresses
+
 import java.io.BufferedWriter
 import java.io.FileNotFoundException
 import java.io.IOException
 import java.io.InputStream
 import java.io.OutputStreamWriter
@@ -11,11 +13,10 @@ import java.nio.channels.SelectionKey
 import java.nio.channels.Selector
 import java.nio.channels.SocketChannel
 import java.util
 import java.util.UUID
 import java.util.concurrent._
-
 import li.cil.oc.Constants
 import li.cil.oc.OpenComputers
 import li.cil.oc.Settings
 import li.cil.oc.api.Network
 import li.cil.oc.api.driver.DeviceInfo
@@ -61,10 +62,13 @@ class InternetCard extends prefab.ManagedEnvironment with DeviceInfo {
 
   @Callback(doc = """function(url:string[, postData:string[, headers:table[, method:string]]]):userdata -- Starts an HTTP request. If this returns true, further results will be pushed using `http_response` signals.""")
   def request(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
     checkOwner(context)
     val address = args.checkString(0)
+    if (!Settings.get.internetAccessAllowed()) {
+      return result(Unit, "internet access is unavailable")
+    }
     if (!Settings.get.httpEnabled) {
       return result(Unit, "http requests are unavailable")
     }
     if (connections.size >= Settings.get.maxConnections) {
       throw new IOException("too many open connections")
@@ -89,10 +93,13 @@ class InternetCard extends prefab.ManagedEnvironment with DeviceInfo {
   @Callback(doc = """function(address:string[, port:number]):userdata -- Opens a new TCP connection. Returns the handle of the connection.""")
   def connect(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
     checkOwner(context)
     val address = args.checkString(0)
     val port = args.optInteger(1, -1)
+    if (!Settings.get.internetAccessAllowed()) {
+      return result(Unit, "internet access is unavailable")
+    }
     if (!Settings.get.tcpEnabled) {
       return result(Unit, "tcp connections are unavailable")
     }
     if (connections.size >= Settings.get.maxConnections) {
       throw new IOException("too many open connections")
@@ -177,11 +184,15 @@ class InternetCard extends prefab.ManagedEnvironment with DeviceInfo {
     url
   }
 }
 
 object InternetCard {
-  private val threadPool = ThreadPoolFactory.create("Internet", Settings.get.internetThreads)
+  // For InternetFilteringRuleTest, where Settings.get is not provided.
+  private val threadPool = ThreadPoolFactory.create("Internet", Option(Settings.get) match {
+    case None => 1
+    case Some(settings) => settings.internetThreads
+  })
 
   trait Closable {
     def close(): Unit
   }
 
@@ -353,16 +364,44 @@ object InternetCard {
       }
     }
 
   }
 
-  def checkLists(inetAddress: InetAddress, host: String) {
-    if (Settings.get.httpHostWhitelist.length > 0 && !Settings.get.httpHostWhitelist.exists(i => i.apply(inetAddress, host).getOrElse(false))) {
-      throw new FileNotFoundException("address is not whitelisted")
+  def isRequestAllowed(settings: Settings, inetAddress: InetAddress, host: String): Boolean = {
+    if (!settings.internetAccessAllowed()) {
+      false
+    } else {
+      val rules = settings.internetFilteringRules
+      inetAddress match {
+        // IPv6 handling
+        case inet6Address: Inet6Address =>
+          // If the IP address is an IPv6 address with an embedded IPv4 address, and the IPv4 address is blocked,
+          // block this request.
+          if (InetAddresses.hasEmbeddedIPv4ClientAddress(inet6Address)) {
+            val inet4in6Address = InetAddresses.getEmbeddedIPv4ClientAddress(inet6Address)
+            if (!rules.map(r => r.apply(inet4in6Address, host)).collectFirst({ case Some(r) => r }).getOrElse(true)) {
+              return false
+            }
+          }
+
+          // Process address as an IPv6 address.
+          rules.map(r => r.apply(inet6Address, host)).collectFirst({ case Some(r) => r }).getOrElse(false)
+        // IPv4 handling
+        case inet4Address: Inet4Address =>
+          // Process address as an IPv4 address.
+          rules.map(r => r.apply(inet4Address, host)).collectFirst({ case Some(r) => r }).getOrElse(false)
+        case _ =>
+          // Unrecognized address type - block.
+          OpenComputers.log.warn("Internet Card blocked unrecognized address type: " + inetAddress.toString)
+          false
+      }
     }
-    if (Settings.get.httpHostBlacklist.length > 0 && Settings.get.httpHostBlacklist.exists(i => i.apply(inetAddress, host).getOrElse(true))) {
-      throw new FileNotFoundException("address is blacklisted")
+  }
+
+  def checkLists(inetAddress: InetAddress, host: String): Unit = {
+    if (!isRequestAllowed(Settings.get, inetAddress, host)) {
+      throw new FileNotFoundException("address is not allowed")
     }
   }
 
   class HTTPRequest extends AbstractValue with Closable {
     def this(owner: InternetCard, url: URL, post: Option[String], headers: Map[String, String], method: Option[String]) {
diff --git a/src/main/scala/li/cil/oc/util/InternetFilteringRule.scala b/src/main/scala/li/cil/oc/util/InternetFilteringRule.scala
new file mode 100644
index 000000000..30837d7f7
--- /dev/null
+++ b/src/main/scala/li/cil/oc/util/InternetFilteringRule.scala
@@ -0,0 +1,125 @@
+package li.cil.oc.util
+
+import com.google.common.net.InetAddresses
+import li.cil.oc.OpenComputers
+
+import java.net.{Inet4Address, Inet6Address, InetAddress}
+import scala.collection.mutable
+
+class InternetFilteringRule(val ruleString: String) {
+  private var _invalid: Boolean = false
+  private val validator: (InetAddress, String) => Option[Boolean] = {
+    try {
+      val ruleParts = ruleString.split(' ')
+      ruleParts.head match {
+        case "allow" | "deny" =>
+          val value = ruleParts.head.equals("allow")
+          val predicates = mutable.MutableList[(InetAddress, String) => Boolean]()
+          ruleParts.tail.foreach(f => {
+            val filter = f.split(":", 2)
+            filter.head match {
+              case "default" =>
+                if (!value) {
+                  predicates += ((_: InetAddress, _: String) => { false })
+                } else {
+                  predicates += ((inetAddress: InetAddress, host: String) => {
+                    InternetFilteringRule.defaultRules.map(r => r.apply(inetAddress, host)).collectFirst({ case Some(r) => r }).getOrElse(false)
+                  })
+                }
+              case "private" =>
+                predicates += ((inetAddress: InetAddress, _: String) => {
+                  inetAddress.isAnyLocalAddress || inetAddress.isLoopbackAddress || inetAddress.isLinkLocalAddress || inetAddress.isSiteLocalAddress
+                })
+              case "bogon" =>
+                predicates += ((inetAddress: InetAddress, _: String) => {
+                  InternetFilteringRule.bogonMatchingRules.exists(rule => rule.matches(inetAddress))
+                })
+              case "ipv4" =>
+                predicates += ((inetAddress: InetAddress, _: String) => {
+                  inetAddress.isInstanceOf[Inet4Address]
+                })
+              case "ipv6" =>
+                predicates += ((inetAddress: InetAddress, _: String) => {
+                  inetAddress.isInstanceOf[Inet6Address]
+                })
+              case "ipv4-embedded-ipv6" =>
+                predicates += ((inetAddress: InetAddress, _: String) => {
+                  inetAddress.isInstanceOf[Inet6Address] && InetAddresses.hasEmbeddedIPv4ClientAddress(inetAddress.asInstanceOf[Inet6Address])
+                })
+              case "domain" =>
+                val domain = filter(1)
+                val addresses = InetAddress.getAllByName(domain)
+                predicates += ((inetAddress: InetAddress, host: String) => {
+                  host == domain || addresses.exists(a => a.equals(inetAddress))
+                })
+              case "ip" =>
+                val ipStringParts = filter(1).split("/", 2)
+                if (ipStringParts.length == 2) {
+                  val ipRange = InetAddressRange.parse(ipStringParts(0), ipStringParts(1))
+                  predicates += ((inetAddress: InetAddress, _: String) => ipRange.matches(inetAddress))
+                } else {
+                  val ipAddress = InetAddresses.forString(ipStringParts(0))
+                  predicates += ((inetAddress: InetAddress, _: String) => ipAddress.equals(inetAddress))
+                }
+                predicates += ((inetAddress: InetAddress, _: String) => {
+                  inetAddress.isAnyLocalAddress || inetAddress.isLoopbackAddress || inetAddress.isLinkLocalAddress || inetAddress.isSiteLocalAddress
+                })
+              case "all" =>
+            }
+          })
+          (inetAddress: InetAddress, host: String) => {
+            if (predicates.forall(p => p(inetAddress, host)))
+              Some(value)
+            else
+              None
+          }
+        case "removeme" =>
+          // Ignore this rule.
+          (_: InetAddress, _: String) => None
+      }
+    } catch {
+      case t: Throwable =>
+        OpenComputers.log.error("Invalid Internet filteringRules rule in configuration: \"" + ruleString + "\".", t)
+        _invalid = true
+        (_: InetAddress, _: String) => Some(false)
+    }
+  }
+
+  def invalid(): Boolean = _invalid
+
+  def apply(inetAddress: InetAddress, host: String) = validator(inetAddress, host)
+}
+
+object InternetFilteringRule {
+  private val defaultRules = Array(
+    new InternetFilteringRule("deny private"),
+    new InternetFilteringRule("deny bogon"),
+    new InternetFilteringRule("allow all")
+  )
+  private val bogonMatchingRules = Array(
+    "0.0.0.0/8",
+    "10.0.0.0/8",
+    "100.64.0.0/10",
+    "127.0.0.0/8",
+    "169.254.0.0/16",
+    "172.16.0.0/12",
+    "192.0.0.0/24",
+    "192.0.2.0/24",
+    "192.168.0.0/16",
+    "198.18.0.0/15",
+    "198.51.100.0/24",
+    "203.0.113.0/24",
+    "224.0.0.0/3",
+    "::/128",
+    "::1/128",
+    "::ffff:0:0/96",
+    "::/96",
+    "100::/64",
+    "2001:10::/28",
+    "2001:db8::/32",
+    "fc00::/7",
+    "fe80::/10",
+    "fec0::/10",
+    "ff00::/8"
+  ).map(s => s.split("/", 2)).map(s => InetAddressRange.parse(s(0), s(1)))
+}
\ No newline at end of file
diff --git a/src/main/scala/li/cil/oc/util/ThreadPoolFactory.scala b/src/main/scala/li/cil/oc/util/ThreadPoolFactory.scala
index 9c22b02b1..5c78c745a 100644
--- a/src/main/scala/li/cil/oc/util/ThreadPoolFactory.scala
+++ b/src/main/scala/li/cil/oc/util/ThreadPoolFactory.scala
@@ -12,11 +12,15 @@ import li.cil.oc.Settings
 
 import scala.collection.mutable
 
 object ThreadPoolFactory {
   val priority = {
-    val custom = Settings.get.threadPriority
+    // For InternetFilteringRuleTest, where Settings.get is not provided.
+    val custom = Option(Settings.get) match {
+      case None => -1
+      case Some(settings) => settings.threadPriority
+    }
     if (custom < 1) Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2
     else custom max Thread.MIN_PRIORITY min Thread.MAX_PRIORITY
   }
 
   def create(name: String, threads: Int) = Executors.newScheduledThreadPool(threads,
diff --git a/src/test/scala/InternetFilteringRuleTest.scala b/src/test/scala/InternetFilteringRuleTest.scala
new file mode 100644
index 000000000..947168931
--- /dev/null
+++ b/src/test/scala/InternetFilteringRuleTest.scala
@@ -0,0 +1,95 @@
+import com.typesafe.config.ConfigFactory
+import li.cil.oc.Settings
+import li.cil.oc.server.component.InternetCard
+import org.junit.runner.RunWith
+import org.scalatest.{FlatSpec, FunSpec, WordSpec}
+import org.scalatest.Matchers.{be, convertToAnyShouldWrapper}
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.mock.MockitoSugar
+
+import java.net.InetAddress
+import scala.compat.Platform.EOL
+import scala.io.{Codec, Source}
+
+@RunWith(classOf[JUnitRunner])
+class InternetFilteringRuleTest extends FunSpec with MockitoSugar {
+  val config = autoClose(classOf[Settings].getResourceAsStream("/application.conf")) { in =>
+    val configStr = Source.fromInputStream(in)(Codec.UTF8).getLines().mkString("", EOL, EOL)
+    ConfigFactory.parseString(configStr)
+  }
+  val settings = new Settings(config.getConfig("opencomputers"))
+
+
+  describe("The default AddressValidators") {
+    // Many of these payloads are pulled from PayloadsAllTheThings
+    // https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Request%20Forgery/README.md
+    it("should accept a valid external address") {
+      isUriBlacklisted("https://google.com") should be(false)
+    }
+    it("should reject localhost") {
+      isUriBlacklisted("http://localhost") should be(true)
+    }
+    it("should reject the local host in IPv4 format") {
+      isUriBlacklisted("http://127.0.0.1") should be(true)
+      isUriBlacklisted("http://127.0.1") should be(true)
+      isUriBlacklisted("http://127.1") should be(true)
+      isUriBlacklisted("http://0") should be (true)
+    }
+    it("should reject the local host in IPv6") {
+      isUriBlacklisted("http://[::1]") should be(true)
+      isUriBlacklisted("http://[::]") should be(true)
+    }
+    it("should reject IPv6/IPv4 Address Embedding") {
+      isUriBlacklisted("http://[0:0:0:0:0:ffff:127.0.0.1]") should be(true)
+      isUriBlacklisted("http://[::ffff:127.0.0.1]") should be(true)
+    }
+    it("should reject an attempt to bypass using a decimal IP location") {
+      isUriBlacklisted("http://2130706433") should be(true) // 127.0.0.1
+      isUriBlacklisted("http://3232235521") should be(true) // 192.168.0.1
+      isUriBlacklisted("http://3232235777") should be(true) // 192.168.1.1
+    }
+    it("should reject the IMDS address in IPv4 format") {
+      isUriBlacklisted("http://169.254.169.254") should be(true)
+      isUriBlacklisted("http://2852039166") should be(true) // 169.254.169.254
+    }
+    it("should reject the IMDS address in IPv6 format") {
+      isUriBlacklisted("http://[fd00:ec2::254]") should be(true)
+    }
+    it("should reject the IMDS in for Oracle Cloud") {
+      isUriBlacklisted("http://192.0.0.192") should be(true)
+    }
+    it("should reject the IMDS in for Alibaba Cloud") {
+      isUriBlacklisted("http://100.100.100.200") should be(true)
+    }
+  }
+
+  def isUriBlacklisted(uri: String): Boolean = {
+    val uriObj = new java.net.URI(uri)
+    val resolved = InetAddress.getByName(uriObj.getHost)
+    !InternetCard.isRequestAllowed(settings, resolved, uriObj.getHost)
+  }
+
+  def autoClose[A <: AutoCloseable, B](closeable: A)(fun: (A) ⇒ B): B = {
+    var t: Throwable = null
+    try {
+      fun(closeable)
+    } catch {
+      case funT: Throwable ⇒
+        t = funT
+        throw t
+    } finally {
+      if (t != null) {
+        try {
+          closeable.close()
+        } catch {
+          case closeT: Throwable ⇒
+            t.addSuppressed(closeT)
+            throw t
+        }
+      } else {
+        closeable.close()
+      }
+    }
+  }
+
+}

Vulnerable code:     # This is a list of forbidden domain names. If an HTTP request is made
    # or a socket connection is opened the target address will be compared
    # to the addresses / address ranges in this list. It it is present in this
    # list, the request will be denied.
    # Entries are either domain names (www.example.com) or IP addresses in
    # string format (10.0.0.3), optionally in CIDR notation to make it easier
    # to define address ranges (1.0.0.0/8). Domains are resolved to their
    # actual IP once on startup, future requests are resolved and compared
    # to the resolved addresses.
    # By default all local addresses are blocked. This is only meant as a
    blacklist: [
      "127.0.0.0/8"
      "0.0.0.0/8"
      "10.0.0.0/8"
      "192.168.0.0/16"
      "172.16.0.0/12"
    # This is a list of allowed domain names. Requests may only be made
    # to addresses that are present in this list. If this list is empty,
    # requests may be made to all addresses not forbidden. Note that the
    # blacklist is always applied, so if an entry is present in both the
    # whitelist and the blacklist, the blacklist will win.
    # Entries are of the same format as in the blacklist. Examples:
    # "gist.github.com", "www.pastebin.com"
    whitelist: []

import java.io._
import java.net.Inet4Address
import java.net.InetAddress
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
import java.util.UUID
import li.cil.oc.integration.Mods
import scala.io.Codec
import scala.io.Source
  val httpHostBlacklist = Array(config.getStringList("internet.blacklist").map(new Settings.AddressValidator(_)): _*)
  val httpHostWhitelist = Array(config.getStringList("internet.whitelist").map(new Settings.AddressValidator(_)): _*)
    val prefix = "opencomputers."
          OpenComputers.log.info(s"Updating setting '$fullPath'. ")
    }
    patched
  }
  val cidrPattern = """(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:/(\d{1,2}))""".r

  class AddressValidator(val value: String) {
    val validator: (InetAddress, String) => Option[Boolean] = try cidrPattern.findFirstIn(value) match {
      case Some(cidrPattern(address, prefix)) =>
        val addr = InetAddresses.coerceToInteger(InetAddresses.forString(address))
        val mask = 0xFFFFFFFF << (32 - prefix.toInt)
        val min = addr & mask
        val max = min | ~mask
        (inetAddress: InetAddress, host: String) => Some(inetAddress match {
          case v4: Inet4Address =>
            val numeric = InetAddresses.coerceToInteger(v4)
            min <= numeric && numeric <= max
          case _ => true // Can't check IPv6 addresses so we pass them.
        })
      case _ =>
        val address = InetAddress.getByName(value)
        (inetAddress: InetAddress, host: String) => Some(host == value || inetAddress == address)
    } catch {
      case t: Throwable =>
        OpenComputers.log.warn("Invalid entry in internet blacklist / whitelist: " + value, t)
        (inetAddress: InetAddress, host: String) => None

    def apply(inetAddress: InetAddress, host: String) = validator(inetAddress, host)

  private val threadPool = ThreadPoolFactory.create("Internet", Settings.get.internetThreads)
  def checkLists(inetAddress: InetAddress, host: String) {
    if (Settings.get.httpHostWhitelist.length > 0 && !Settings.get.httpHostWhitelist.exists(i => i.apply(inetAddress, host).getOrElse(false))) {
      throw new FileNotFoundException("address is not whitelisted")
    if (Settings.get.httpHostBlacklist.length > 0 && Settings.get.httpHostBlacklist.exists(i => i.apply(inetAddress, host).getOrElse(true))) {
      throw new FileNotFoundException("address is blacklisted")
    val custom = Settings.get.threadPriority
Fixed code: * Reworked Internet Card filtering rules.
  * Implemented a new, more powerful system and improved default configuration.
  * Internet Card rules are now stored in the "internet.filteringRules" configuration key.
  * The old keys ("internet.whitelist", "internet.blacklist") are no longer used; an automatic migration is done upon upgrading the mod.
package com.typesafe.config.impl;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigValue;
import org.luaj.vm2.ast.Str;

import java.util.List;

public final class OpenComputersConfigCommentManipulationHook {
    private OpenComputersConfigCommentManipulationHook() {

    }

    public static Config setComments(Config config, String path, List<String> comments) {
        return config.withValue(path, setComments(config.getValue(path), comments));
    }

    public static ConfigValue setComments(ConfigValue value, List<String> comments) {
        if (value.origin() instanceof SimpleConfigOrigin && value instanceof AbstractConfigValue) {
            return ((AbstractConfigValue) value).withOrigin(
                ((SimpleConfigOrigin) value.origin()).setComments(comments)
            );
        } else {
            return value;
        }
    }
}

package li.cil.oc.util;

import com.google.common.net.InetAddresses;

import java.net.InetAddress;

// Originally by SquidDev
public final class InetAddressRange {
    private final byte[] min;
    private final byte[] max;

    InetAddressRange(byte[] min, byte[] max) {
        this.min = min;
        this.max = max;
    }

    public boolean matches(InetAddress address) {
        byte[] entry = address.getAddress();
        if (entry.length != min.length) return false;

        for (int i = 0; i < entry.length; i++) {
            int value = 0xFF & entry[i];
            if (value < (0xFF & min[i]) || value > (0xFF & max[i])) return false;
        }

        return true;
    }

    public static InetAddressRange parse(String addressStr, String prefixSizeStr) {
        int prefixSize;
        try {
            prefixSize = Integer.parseInt(prefixSizeStr);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(String.format("Malformed address range entry '%s': Cannot extract size of CIDR mask from '%s'.",
                    addressStr + '/' + prefixSizeStr, prefixSizeStr));
        }

        InetAddress address;
        try {
            address = InetAddresses.forString(addressStr);
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException(String.format("Malformed address range entry '%s': Cannot extract IP address from '%s'.",
                    addressStr + '/' + prefixSizeStr, addressStr));
        }

        // Mask the bytes of the IP address.
        byte[] minBytes = address.getAddress(), maxBytes = address.getAddress();
        int size = prefixSize;
        for (int i = 0; i < minBytes.length; i++) {
            if (size <= 0) {
                minBytes[i] = (byte) 0;
                maxBytes[i] = (byte) 0xFF;
            } else if (size < 8) {
                minBytes[i] = (byte) (minBytes[i] & 0xFF << (8 - size));
                maxBytes[i] = (byte) (maxBytes[i] | ~(0xFF << (8 - size)));
            }

            size -= 8;
        }

        return new InetAddressRange(minBytes, maxBytes);
    }
}
    # This is a list of filtering rules. For any HTTP request or TCP socket
    # connection, the target address will be processed by each rule, starting
    # from first to last. The first matching rule will be applied; if no rule
    # contains a match, the request will be denied.
    # Two types of rules are currently supported: "allow", which allows an
    # address to be accessed, and "deny", which forbids such access.
    # Rules can be suffixed with additional filters to limit their scope:
    # - all: apply to all addresses
    # - default: apply built-in allow/deny rules; these may not be up to date,
    #   so one should primarily rely on them as a fallback
    # - private: apply to all private addresses
    # - bogon: apply to all known bogon addresses
    # - ipv4: apply to all IPv4 addresses
    # - ipv6: apply to all IPv6 addresses
    # - ipv4-embedded-ipv6: apply to all IPv4 addresses embedded in IPv6
    #   addresses
    # - ip:[address]: apply to this IP address in string format (10.0.0.3).
    #   CIDR notation is supported and allows defining address ranges
    #   (1.0.0.0/8).
    # - domain:[domain]: apply to this domain. Domains are resolved to their
    #   actual IP only once (on startup), future requests are resolved and
    #   compared to the resolved addresses. Wildcards are not supported.
    # The "removeme" rule does not have any use, but is instead present to
    # detect whether to emit a warning on dedicated server configurations.
    # Modpack authors are asked not to remove this rule; server administrators
    # are free to remove it once the filtering rules have been adjusted.
    # By default all private addresses are blocked. This is only meant as a
    filteringRules: [
      "removeme",
      "deny private",
      "deny bogon",
      "allow default"

    if (Settings.get.internetAccessConfigured()) {
      if (Settings.get.internetFilteringRulesInvalid()) {
        OpenComputers.log.warn("####################################################")
        OpenComputers.log.warn("#                                                  #")
        OpenComputers.log.warn("#  Could not parse Internet Card filtering rules!  #")
        OpenComputers.log.warn("#  Review the server log and adjust the filtering  #")
        OpenComputers.log.warn("#  list to ensure it is appropriately configured.  #")
        OpenComputers.log.warn("#   (config/OpenComputers.cfg => filteringRules)   #")
        OpenComputers.log.warn("# Internet access has been automatically disabled. #")
        OpenComputers.log.warn("#                                                  #")
        OpenComputers.log.warn("####################################################")
      } else if (!Settings.get.internetFilteringRulesObserved && e.getServer.isDedicatedServer) {
        OpenComputers.log.warn("####################################################")
        OpenComputers.log.warn("#                                                  #")
        OpenComputers.log.warn("#    It appears that you're running a dedicated    #")
        OpenComputers.log.warn("#  server with OpenComputers installed! Make sure  #")
        OpenComputers.log.warn("#  to review the Internet Card address filtering   #")
        OpenComputers.log.warn("#  list to ensure it is appropriately configured.  #")
        OpenComputers.log.warn("#   (config/OpenComputers.cfg => filteringRules)   #")
        OpenComputers.log.warn("#                                                  #")
        OpenComputers.log.warn("####################################################")
      } else {
        OpenComputers.log.info(f"Successfully applied ${Settings.get.internetFilteringRules.length} Internet Card filtering rules.")
      }
    }
import com.typesafe.config.impl.OpenComputersConfigCommentManipulationHook
import li.cil.oc.util.{InetAddressRange, InternetFilteringRule}
import java.io._
import java.net.{Inet4Address, Inet6Address, InetAddress}
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
import java.util.UUID
import scala.collection.JavaConverters._
import scala.io.{Codec, Source}
  val internetFilteringRules = Array(config.getStringList("internet.filteringRules")
    .filter(p => !p.equals("removeme"))
    .map(new InternetFilteringRule(_)): _*)
  val internetFilteringRulesObserved = !config.getStringList("internet.filteringRules")
    .contains("removeme")

  def internetFilteringRulesInvalid(): Boolean = {
    internetFilteringRules.exists(p => p.invalid())
  }

  def internetAccessConfigured(): Boolean = {
    httpEnabled || tcpEnabled
  }

  def internetAccessAllowed(): Boolean = {
    internetAccessConfigured() && !internetFilteringRulesInvalid()
  }
  private val forbiddenConfigLists: List[String] = List(
    /* 1.8.3+ filtering rules migration */
    "internet.blacklist", "internet.whitelist"
  )
  private val prefix = "opencomputers."
    for (key <- forbiddenConfigLists) {
      if (config.hasPath(prefix + key)) {
        if (!config.getStringList(prefix + key).isEmpty) {
          throw new RuntimeException("Error parsing configuration file: removed configuration option '" + key + "' is not empty. This option should no longer be used.")
        }
      }
    }
  private val fileringRulesPatchVersion = VersionRange.createFromVersionSpec("[0.0, 1.8.3)")
          OpenComputers.log.info(s"=> Updating setting '$fullPath'. ")
      // Migrate filtering rules to 1.8.3+
      if (fileringRulesPatchVersion.containsVersion(configVersion)) {
        OpenComputers.log.info(s"=> Migrating Internet Card filtering rules. ")
        val cidrPattern = """(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:/(\d{1,2}))""".r
        val httpHostWhitelist = patched.getStringList(prefix + "internet.whitelist")
        val httpHostBlacklist = patched.getStringList(prefix + "internet.blacklist")
        val internetFilteringRules = mutable.MutableList[String]()
        for (blockedAddress <- httpHostBlacklist) {
          if (cidrPattern.findFirstIn(blockedAddress).isDefined) {
            internetFilteringRules += "deny ip:" + blockedAddress
          } else {
            internetFilteringRules += "deny domain:" + blockedAddress
          }
        }
        for (allowedAddress <- httpHostWhitelist) {
          if (cidrPattern.findFirstIn(allowedAddress).isDefined) {
            internetFilteringRules += "allow ip:" + allowedAddress
          } else {
            internetFilteringRules += "allow domain:" + allowedAddress
          }
        }
        if (!httpHostWhitelist.isEmpty) {
          internetFilteringRules += "deny all"
        }
        for (defaultRule <- defaults.getStringList(prefix + "internet.filteringRules")) {
          internetFilteringRules += defaultRule
        }
        var patchedRules: ConfigValue = ConfigValueFactory.fromIterable(internetFilteringRules.asJava)
        // We need to use the private APIs here, unfortunately.
        try {
          for (key <- List("internet.whitelist", "internet.blacklist")) {
            if (patched.hasPath(prefix + key)) {
              val originalValue = patched.getValue(prefix + key)
              var deprecatedValue: ConfigValue = ConfigValueFactory.fromIterable(new java.util.ArrayList[String](), originalValue.origin().description())
              val comments = mutable.MutableList("No longer used! See internet.filteringRules.", "", "Previous contents:")
              for (value <- patched.getStringList(prefix + key)) {
                comments += "\"" + value + "\""
              }
              deprecatedValue = OpenComputersConfigCommentManipulationHook.setComments(deprecatedValue, comments.asJava)
              patched = patched.withValue(prefix + key, deprecatedValue)
            }
          }
          patchedRules = OpenComputersConfigCommentManipulationHook.setComments(
            patchedRules, defaults.getValue(prefix + "internet.filteringRules").origin().comments()
          )
        } catch {
          case _: Throwable => /* pass */
        }
        patched = patched.withValue(prefix + "internet.filteringRules", patchedRules)
      }
    patched
import com.google.common.net.InetAddresses

    if (!Settings.get.internetAccessAllowed()) {
      return result(Unit, "internet access is unavailable")
    }
    if (!Settings.get.internetAccessAllowed()) {
      return result(Unit, "internet access is unavailable")
    }
  // For InternetFilteringRuleTest, where Settings.get is not provided.
  private val threadPool = ThreadPoolFactory.create("Internet", Option(Settings.get) match {
    case None => 1
    case Some(settings) => settings.internetThreads
  })
  def isRequestAllowed(settings: Settings, inetAddress: InetAddress, host: String): Boolean = {
    if (!settings.internetAccessAllowed()) {
      false
    } else {
      val rules = settings.internetFilteringRules
      inetAddress match {
        // IPv6 handling
        case inet6Address: Inet6Address =>
          // If the IP address is an IPv6 address with an embedded IPv4 address, and the IPv4 address is blocked,
          // block this request.
          if (InetAddresses.hasEmbeddedIPv4ClientAddress(inet6Address)) {
            val inet4in6Address = InetAddresses.getEmbeddedIPv4ClientAddress(inet6Address)
            if (!rules.map(r => r.apply(inet4in6Address, host)).collectFirst({ case Some(r) => r }).getOrElse(true)) {
              return false
            }
          }

          // Process address as an IPv6 address.
          rules.map(r => r.apply(inet6Address, host)).collectFirst({ case Some(r) => r }).getOrElse(false)
        // IPv4 handling
        case inet4Address: Inet4Address =>
          // Process address as an IPv4 address.
          rules.map(r => r.apply(inet4Address, host)).collectFirst({ case Some(r) => r }).getOrElse(false)
        case _ =>
          // Unrecognized address type - block.
          OpenComputers.log.warn("Internet Card blocked unrecognized address type: " + inetAddress.toString)
          false
      }
  }

  def checkLists(inetAddress: InetAddress, host: String): Unit = {
    if (!isRequestAllowed(Settings.get, inetAddress, host)) {
      throw new FileNotFoundException("address is not allowed")
package li.cil.oc.util

import com.google.common.net.InetAddresses
import li.cil.oc.OpenComputers

import java.net.{Inet4Address, Inet6Address, InetAddress}
import scala.collection.mutable

class InternetFilteringRule(val ruleString: String) {
  private var _invalid: Boolean = false
  private val validator: (InetAddress, String) => Option[Boolean] = {
    try {
      val ruleParts = ruleString.split(' ')
      ruleParts.head match {
        case "allow" | "deny" =>
          val value = ruleParts.head.equals("allow")
          val predicates = mutable.MutableList[(InetAddress, String) => Boolean]()
          ruleParts.tail.foreach(f => {
            val filter = f.split(":", 2)
            filter.head match {
              case "default" =>
                if (!value) {
                  predicates += ((_: InetAddress, _: String) => { false })
                } else {
                  predicates += ((inetAddress: InetAddress, host: String) => {
                    InternetFilteringRule.defaultRules.map(r => r.apply(inetAddress, host)).collectFirst({ case Some(r) => r }).getOrElse(false)
                  })
                }
              case "private" =>
                predicates += ((inetAddress: InetAddress, _: String) => {
                  inetAddress.isAnyLocalAddress || inetAddress.isLoopbackAddress || inetAddress.isLinkLocalAddress || inetAddress.isSiteLocalAddress
                })
              case "bogon" =>
                predicates += ((inetAddress: InetAddress, _: String) => {
                  InternetFilteringRule.bogonMatchingRules.exists(rule => rule.matches(inetAddress))
                })
              case "ipv4" =>
                predicates += ((inetAddress: InetAddress, _: String) => {
                  inetAddress.isInstanceOf[Inet4Address]
                })
              case "ipv6" =>
                predicates += ((inetAddress: InetAddress, _: String) => {
                  inetAddress.isInstanceOf[Inet6Address]
                })
              case "ipv4-embedded-ipv6" =>
                predicates += ((inetAddress: InetAddress, _: String) => {
                  inetAddress.isInstanceOf[Inet6Address] && InetAddresses.hasEmbeddedIPv4ClientAddress(inetAddress.asInstanceOf[Inet6Address])
                })
              case "domain" =>
                val domain = filter(1)
                val addresses = InetAddress.getAllByName(domain)
                predicates += ((inetAddress: InetAddress, host: String) => {
                  host == domain || addresses.exists(a => a.equals(inetAddress))
                })
              case "ip" =>
                val ipStringParts = filter(1).split("/", 2)
                if (ipStringParts.length == 2) {
                  val ipRange = InetAddressRange.parse(ipStringParts(0), ipStringParts(1))
                  predicates += ((inetAddress: InetAddress, _: String) => ipRange.matches(inetAddress))
                } else {
                  val ipAddress = InetAddresses.forString(ipStringParts(0))
                  predicates += ((inetAddress: InetAddress, _: String) => ipAddress.equals(inetAddress))
                }
                predicates += ((inetAddress: InetAddress, _: String) => {
                  inetAddress.isAnyLocalAddress || inetAddress.isLoopbackAddress || inetAddress.isLinkLocalAddress || inetAddress.isSiteLocalAddress
                })
              case "all" =>
            }
          })
          (inetAddress: InetAddress, host: String) => {
            if (predicates.forall(p => p(inetAddress, host)))
              Some(value)
            else
              None
          }
        case "removeme" =>
          // Ignore this rule.
          (_: InetAddress, _: String) => None
      }
    } catch {
      case t: Throwable =>
        OpenComputers.log.error("Invalid Internet filteringRules rule in configuration: \"" + ruleString + "\".", t)
        _invalid = true
        (_: InetAddress, _: String) => Some(false)
    }
  }

  def invalid(): Boolean = _invalid

  def apply(inetAddress: InetAddress, host: String) = validator(inetAddress, host)
}

object InternetFilteringRule {
  private val defaultRules = Array(
    new InternetFilteringRule("deny private"),
    new InternetFilteringRule("deny bogon"),
    new InternetFilteringRule("allow all")
  )
  private val bogonMatchingRules = Array(
    "0.0.0.0/8",
    "10.0.0.0/8",
    "100.64.0.0/10",
    "127.0.0.0/8",
    "169.254.0.0/16",
    "172.16.0.0/12",
    "192.0.0.0/24",
    "192.0.2.0/24",
    "192.168.0.0/16",
    "198.18.0.0/15",
    "198.51.100.0/24",
    "203.0.113.0/24",
    "224.0.0.0/3",
    "::/128",
    "::1/128",
    "::ffff:0:0/96",
    "::/96",
    "100::/64",
    "2001:10::/28",
    "2001:db8::/32",
    "fc00::/7",
    "fe80::/10",
    "fec0::/10",
    "ff00::/8"
  ).map(s => s.split("/", 2)).map(s => InetAddressRange.parse(s(0), s(1)))
}
    // For InternetFilteringRuleTest, where Settings.get is not provided.
    val custom = Option(Settings.get) match {
      case None => -1
      case Some(settings) => settings.threadPriority
    }
import com.typesafe.config.ConfigFactory
import li.cil.oc.Settings
import li.cil.oc.server.component.InternetCard
import org.junit.runner.RunWith
import org.scalatest.{FlatSpec, FunSpec, WordSpec}
import org.scalatest.Matchers.{be, convertToAnyShouldWrapper}
import org.scalatest.junit.JUnitRunner
import org.scalatest.mock.MockitoSugar

import java.net.InetAddress
import scala.compat.Platform.EOL
import scala.io.{Codec, Source}

@RunWith(classOf[JUnitRunner])
class InternetFilteringRuleTest extends FunSpec with MockitoSugar {
  val config = autoClose(classOf[Settings].getResourceAsStream("/application.conf")) { in =>
    val configStr = Source.fromInputStream(in)(Codec.UTF8).getLines().mkString("", EOL, EOL)
    ConfigFactory.parseString(configStr)
  }
  val settings = new Settings(config.getConfig("opencomputers"))


  describe("The default AddressValidators") {
    // Many of these payloads are pulled from PayloadsAllTheThings
    // https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Request%20Forgery/README.md
    it("should accept a valid external address") {
      isUriBlacklisted("https://google.com") should be(false)
    }
    it("should reject localhost") {
      isUriBlacklisted("http://localhost") should be(true)
    }
    it("should reject the local host in IPv4 format") {
      isUriBlacklisted("http://127.0.0.1") should be(true)
      isUriBlacklisted("http://127.0.1") should be(true)
      isUriBlacklisted("http://127.1") should be(true)
      isUriBlacklisted("http://0") should be (true)
    }
    it("should reject the local host in IPv6") {
      isUriBlacklisted("http://[::1]") should be(true)
      isUriBlacklisted("http://[::]") should be(true)
    }
    it("should reject IPv6/IPv4 Address Embedding") {
      isUriBlacklisted("http://[0:0:0:0:0:ffff:127.0.0.1]") should be(true)
      isUriBlacklisted("http://[::ffff:127.0.0.1]") should be(true)
    }
    it("should reject an attempt to bypass using a decimal IP location") {
      isUriBlacklisted("http://2130706433") should be(true) // 127.0.0.1
      isUriBlacklisted("http://3232235521") should be(true) // 192.168.0.1
      isUriBlacklisted("http://3232235777") should be(true) // 192.168.1.1
    }
    it("should reject the IMDS address in IPv4 format") {
      isUriBlacklisted("http://169.254.169.254") should be(true)
      isUriBlacklisted("http://2852039166") should be(true) // 169.254.169.254
    }
    it("should reject the IMDS address in IPv6 format") {
      isUriBlacklisted("http://[fd00:ec2::254]") should be(true)
    }
    it("should reject the IMDS in for Oracle Cloud") {
      isUriBlacklisted("http://192.0.0.192") should be(true)
    }
    it("should reject the IMDS in for Alibaba Cloud") {
      isUriBlacklisted("http://100.100.100.200") should be(true)
    }
  }

  def isUriBlacklisted(uri: String): Boolean = {
    val uriObj = new java.net.URI(uri)
    val resolved = InetAddress.getByName(uriObj.getHost)
    !InternetCard.isRequestAllowed(settings, resolved, uriObj.getHost)
  }

  def autoClose[A <: AutoCloseable, B](closeable: A)(fun: (A) ⇒ B): B = {
    var t: Throwable = null
    try {
      fun(closeable)
    } catch {
      case funT: Throwable ⇒
        t = funT
        throw t
    } finally {
      if (t != null) {
        try {
          closeable.close()
        } catch {
          case closeT: Throwable ⇒
            t.addSuppressed(closeT)
            throw t
        }
      } else {
        closeable.close()
      }
    }
  }

}