commit 05d1676032a732c72fb03ac358f8e3cdbb498a95
parent c344fa59bdcf3bfae588b4d9626dc282bb9999f8
Author: Matthias Jaros <jaros@mailbox.org>
Date: Thu, 22 Jan 2026 21:05:16 +0100
Working version with semaphore counter and rate limiter
Diffstat:
8 files changed, 252 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+# java
+/target
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
diff --git a/.mvn/maven.config b/.mvn/maven.config
diff --git a/pom.xml b/pom.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>com.it-jaros</groupId>
+ <artifactId>networkScanner</artifactId>
+ <version>1.0-SNAPSHOT</version>
+
+ <name>networkScanner</name>
+ <!-- FIXME change it to the project's website -->
+ <url>http://www.example.com</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <maven.compiler.release>21</maven.compiler.release>
+ </properties>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.junit</groupId>
+ <artifactId>junit-bom</artifactId>
+ <version>5.11.0</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <!-- Optionally: parameterized tests support -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-params</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
+ <plugins>
+ <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
+ <plugin>
+ <artifactId>maven-clean-plugin</artifactId>
+ <version>3.4.0</version>
+ </plugin>
+ <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
+ <plugin>
+ <artifactId>maven-resources-plugin</artifactId>
+ <version>3.3.1</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.13.0</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>3.3.0</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>3.4.2</version>
+ <configuration>
+ <archive>
+ <manifest>
+ <mainClass>com.it_jaros.networkScanner.App</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-install-plugin</artifactId>
+ <version>3.1.2</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <version>3.1.2</version>
+ </plugin>
+ <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
+ <plugin>
+ <artifactId>maven-site-plugin</artifactId>
+ <version>3.12.1</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-project-info-reports-plugin</artifactId>
+ <version>3.6.1</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+</project>
diff --git a/src/main/java/com/it_jaros/networkScanner/App.java b/src/main/java/com/it_jaros/networkScanner/App.java
@@ -0,0 +1,41 @@
+package com.it_jaros.networkScanner;
+
+import java.util.List;
+
+public class App {
+ public static void main(String[] args) {
+ Scanner scanner = new Scanner();
+ if (args.length < 1 || args[0] == "") {
+ usageAndExit();
+ }
+ String target = args[0];
+ System.out.printf(
+ "Scanning host=%s \n",
+ target);
+
+ try (FdCounter fd = new FdCounter()) {
+ fd.start(20); // sample every 20ms
+ List<Integer> openPorts = scanner.scanPorts(target, 10000);
+ printPorts(target, openPorts);
+ System.out.println("FD limit: " + fd.maxFds());
+ System.out.println("Peak open FDs during scan: " + fd.peakOpenFds());
+ }
+ }
+
+ public static void printPorts(String address, List<Integer> openPorts) {
+ System.out.println("Address:" + address);
+ openPorts.forEach((p) -> System.out.println("Open port: " + p));
+ }
+
+ private static void usageAndExit() {
+ System.err.println("""
+ Usage:
+ java -jar scanner.jar <host>
+
+ Examples:
+ java -jar scanner.jar localhost
+ java -jar scanner.jar fd00::1
+ """);
+ System.exit(1);
+ }
+}
diff --git a/src/main/java/com/it_jaros/networkScanner/Counter.java b/src/main/java/com/it_jaros/networkScanner/Counter.java
@@ -0,0 +1,25 @@
+package com.it_jaros.networkScanner;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class Counter {
+ private final AtomicInteger current = new AtomicInteger(0);
+ private final AtomicInteger max = new AtomicInteger(0);
+
+ public void inc() {
+ int now = current.incrementAndGet();
+ max.accumulateAndGet(now, Math::max);
+ }
+
+ public void dec() {
+ current.decrementAndGet();
+ }
+
+ public int current() {
+ return current.get();
+ }
+
+ public int max() {
+ return max.get();
+ }
+}
diff --git a/src/main/java/com/it_jaros/networkScanner/FdCounter.java b/src/main/java/com/it_jaros/networkScanner/FdCounter.java
@@ -0,0 +1,37 @@
+package com.it_jaros.networkScanner;
+
+import java.lang.management.ManagementFactory;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import com.sun.management.UnixOperatingSystemMXBean;
+
+public final class FdCounter implements AutoCloseable {
+ private final UnixOperatingSystemMXBean os =
+ (UnixOperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
+ private final ScheduledExecutorService ses =
+ Executors.newSingleThreadScheduledExecutor(Thread.ofPlatform().daemon().factory());
+ private final AtomicLong peak = new AtomicLong(0);
+
+ public void start(long everyMillis) {
+ ses.scheduleAtFixedRate(() -> {
+ long now = os.getOpenFileDescriptorCount();
+ peak.accumulateAndGet(now, Math::max);
+ }, 0, everyMillis, TimeUnit.MILLISECONDS);
+ }
+
+ public long peakOpenFds() {
+ return peak.get();
+ }
+
+ public long maxFds() {
+ return os.getMaxFileDescriptorCount();
+ }
+
+ @Override public void close() {
+ ses.shutdownNow();
+ }
+}
+
diff --git a/src/main/java/com/it_jaros/networkScanner/Scanner.java b/src/main/java/com/it_jaros/networkScanner/Scanner.java
@@ -0,0 +1,50 @@
+package com.it_jaros.networkScanner;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+
+public class Scanner {
+
+ private Counter counter = new Counter();
+ private final int socketLimitDefault = 1024;
+
+ public Scanner() {}
+
+ public List<Integer> scanPorts(String address) {
+ return scanPorts(address, socketLimitDefault);
+ }
+
+ public List<Integer> scanPorts(String address, int socketLimit) {
+ Queue<Integer> openPorts = new ConcurrentLinkedQueue<>();
+ Semaphore socketCap = new Semaphore(socketLimit);
+ try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+ for (int port = 1; port < 65536; port++) {
+ final int currentPort = port;
+ executor.submit(() -> {
+ socketCap.acquireUninterruptibly();
+ counter.inc();
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress(address, currentPort));
+ openPorts.add(currentPort);
+ } catch (ConnectException ignored) {
+ } catch (IOException e) {
+ System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
+ } finally {
+ counter.dec();
+ socketCap.release();
+ }
+ });
+ }
+ }
+ System.out.println("Peak concurrent connects: " + counter.max());
+ return openPorts.stream().toList();
+ }
+}