Merge pull request #321 from atoulme/add_ipchecks
Add support for IP range checks
diff --git a/dependency-versions.gradle b/dependency-versions.gradle
index 3875d70..6f08571 100644
--- a/dependency-versions.gradle
+++ b/dependency-versions.gradle
@@ -14,6 +14,7 @@
dependencies {
dependency('ch.qos.logback:logback-classic:1.2.3')
dependency('commons-codec:commons-codec:1.14')
+ dependency('commons-net:commons-net:3.8.0')
dependency('com.fasterxml.jackson.core:jackson-databind:2.11.0')
dependency('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.0')
dependency('com.github.jnr:jnr-ffi:2.1.9')
diff --git a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt
index 782d644..b2ba1c7 100644
--- a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt
+++ b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt
@@ -27,6 +27,7 @@
import org.apache.tuweni.jsonrpc.methods.MeteredHandler
import org.apache.tuweni.jsonrpc.methods.MethodAllowListHandler
import org.apache.tuweni.metrics.MetricsService
+import org.apache.tuweni.net.ip.IPRangeChecker
import org.apache.tuweni.net.tls.VertxTrustOptions
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.slf4j.LoggerFactory
@@ -90,14 +91,15 @@
config.basicAuthUsername(),
config.basicAuthPassword(),
config.basicAuthRealm(),
- methodHandler = handler::handleRequest,
+ IPRangeChecker.create(config.allowedRanges(), config.rejectedRanges()),
Executors.newFixedThreadPool(
config.numberOfThreads()
) {
val thread = Thread("jsonrpc")
thread.isDaemon = true
thread
- }.asCoroutineDispatcher()
+ }.asCoroutineDispatcher(),
+ handler::handleRequest
)
Runtime.getRuntime().addShutdownHook(
Thread {
diff --git a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt
index 8977dd3..130efc8 100644
--- a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt
+++ b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt
@@ -45,6 +45,8 @@
.addString("basicAuthPassword", null, "HTTP Basic Auth password", null)
.addString("basicAuthRealm", null, "HTTP Basic Auth realm", null)
.addListOfString("allowedMethods", Collections.emptyList(), "Allowed JSON-RPC methods", null)
+ .addListOfString("allowedRanges", Collections.singletonList("0.0.0.0/0"), "Allowed IP ranges", null)
+ .addListOfString("rejectedRanges", Collections.emptyList(), "Rejected IP ranges", null)
.toSchema()
}
@@ -65,4 +67,6 @@
fun basicAuthPassword() = config.getString("basicAuthPassword")
fun basicAuthRealm() = config.getString("basicAuthRealm")
fun allowedMethods() = config.getListOfString("allowedMethods")
+ fun allowedRanges() = config.getListOfString("allowedRanges")
+ fun rejectedRanges() = config.getListOfString("rejectedRanges")
}
diff --git a/jsonrpc/build.gradle b/jsonrpc/build.gradle
index 14bdac7..e2d2da2 100644
--- a/jsonrpc/build.gradle
+++ b/jsonrpc/build.gradle
@@ -29,6 +29,7 @@
implementation project(':crypto')
implementation project(':concurrent')
implementation project(':eth')
+ implementation project(':net')
implementation project(':units')
testImplementation project(':io')
diff --git a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCServer.kt b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCServer.kt
index 5c6ffbe..1a05e89 100644
--- a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCServer.kt
+++ b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCServer.kt
@@ -47,6 +47,7 @@
import org.apache.tuweni.eth.JSONRPCResponse
import org.apache.tuweni.eth.internalError
import org.apache.tuweni.eth.parseError
+import org.apache.tuweni.net.ip.IPRangeChecker
import org.slf4j.LoggerFactory
import java.io.IOException
import java.lang.IllegalArgumentException
@@ -62,8 +63,9 @@
val basicAuthenticationUsername: String? = null,
val basicAuthenticationPassword: String? = null,
val basicAuthRealm: String = "Apache Tuweni JSON-RPC proxy",
- val methodHandler: (JSONRPCRequest) -> JSONRPCResponse,
+ val ipRangeChecker: IPRangeChecker = IPRangeChecker.allowAll(),
override val coroutineContext: CoroutineContext = Dispatchers.Default,
+ val methodHandler: (JSONRPCRequest) -> JSONRPCResponse,
) : CoroutineScope {
companion object {
@@ -94,6 +96,13 @@
serverOptions.setTrustOptions(it)
}
httpServer = vertx.createHttpServer()
+ httpServer?.connectionHandler {
+ val remoteAddress = it.remoteAddress().hostAddress()
+ if (!ipRangeChecker.check(remoteAddress)) {
+ logger.debug("Rejecting IP {}", remoteAddress)
+ it.close()
+ }
+ }
httpServer?.exceptionHandler {
logger.error(it.message, it)
}
diff --git a/net/build.gradle b/net/build.gradle
index e419957..f63a270 100644
--- a/net/build.gradle
+++ b/net/build.gradle
@@ -17,6 +17,7 @@
implementation project(':crypto')
implementation project(':io')
implementation 'com.google.guava:guava'
+ implementation 'commons-net:commons-net'
compileOnly 'io.vertx:vertx-core'
compileOnly 'org.bouncycastle:bcprov-jdk15on'
compileOnly 'org.bouncycastle:bcpkix-jdk15on'
diff --git a/net/src/main/java/org/apache/tuweni/net/ip/IPRangeChecker.java b/net/src/main/java/org/apache/tuweni/net/ip/IPRangeChecker.java
new file mode 100644
index 0000000..8ef4b9f
--- /dev/null
+++ b/net/src/main/java/org/apache/tuweni/net/ip/IPRangeChecker.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.tuweni.net.ip;
+
+import org.apache.commons.net.util.SubnetUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Checks that an IP is allowed according to IP ranges.
+ */
+public class IPRangeChecker {
+
+ /**
+ * Creates a new IP range checker.
+ *
+ * @param allowedRanges list of allowed ranges.
+ * @param rejectedRanges list of rejected ranges
+ * @return a new IP range checker
+ * @throws IllegalArgumentException if a range is invalid.
+ */
+ public static IPRangeChecker create(List<String> allowedRanges, List<String> rejectedRanges) {
+ List<SubnetUtils> allowed = new ArrayList<>();
+ List<SubnetUtils> rejected = new ArrayList<>();
+ for (String iprange : allowedRanges) {
+ allowed.add(new SubnetUtils(iprange));
+ }
+ for (String iprange : rejectedRanges) {
+ rejected.add(new SubnetUtils(iprange));
+ }
+ return new IPRangeChecker(allowed, rejected);
+ }
+
+ /**
+ * Creates a checker that allows any IP.
+ *
+ * @return a new range checker that allows any IP.
+ */
+ public static IPRangeChecker allowAll() {
+ return create(Collections.singletonList("0.0.0.0/0"), Collections.emptyList());
+ }
+
+ private final List<SubnetUtils> allowedRanges;
+ private final List<SubnetUtils> rejectedRanges;
+
+ private IPRangeChecker(List<SubnetUtils> allowedRanges, List<SubnetUtils> rejectedRanges) {
+ this.allowedRanges = allowedRanges;
+ this.rejectedRanges = rejectedRanges;
+ }
+
+ /**
+ * Checks if an IP address is inside the ranges of this checker
+ *
+ * @param ip the IP address to check
+ * @return true if it is inside the ranges of the checker.
+ */
+ public boolean check(String ip) {
+ for (SubnetUtils subnetUtils : allowedRanges) {
+ if (!subnetUtils.getInfo().isInRange(ip)) {
+ return false;
+ }
+ }
+ for (SubnetUtils subnetUtils : rejectedRanges) {
+ if (subnetUtils.getInfo().isInRange(ip)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+}
diff --git a/net/src/test/java/org/apache/tuweni/net/ip/IPRangerCheckerTest.java b/net/src/test/java/org/apache/tuweni/net/ip/IPRangerCheckerTest.java
new file mode 100644
index 0000000..3658e4a
--- /dev/null
+++ b/net/src/test/java/org/apache/tuweni/net/ip/IPRangerCheckerTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.tuweni.net.ip;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+class IPRangerCheckerTest {
+
+ @Test
+ void testAllowedAll() {
+ IPRangeChecker checker = IPRangeChecker.allowAll();
+ assertTrue(checker.check("127.0.0.1"));
+ assertTrue(checker.check("192.168.0.1"));
+ assertTrue(checker.check("10.0.10.1"));
+ }
+
+ @Test
+ void testRejectRange() {
+ IPRangeChecker checker =
+ IPRangeChecker.create(Collections.singletonList("0.0.0.0/0"), Collections.singletonList("10.0.0.0/24"));
+ assertTrue(checker.check("127.0.0.1"));
+ assertTrue(checker.check("192.168.0.1"));
+ assertFalse(checker.check("10.0.0.2"));
+ }
+}