summaryrefslogtreecommitdiff
path: root/src/main/java/com/it_jaros/network_scanner
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/it_jaros/network_scanner')
-rw-r--r--src/main/java/com/it_jaros/network_scanner/App.java36
-rw-r--r--src/main/java/com/it_jaros/network_scanner/CliParser.java80
-rw-r--r--src/main/java/com/it_jaros/network_scanner/Counter.java25
-rw-r--r--src/main/java/com/it_jaros/network_scanner/PortRange.java91
-rw-r--r--src/main/java/com/it_jaros/network_scanner/Progress.java5
-rw-r--r--src/main/java/com/it_jaros/network_scanner/ProgressBar.java75
-rw-r--r--src/main/java/com/it_jaros/network_scanner/ScanOptions.java5
-rw-r--r--src/main/java/com/it_jaros/network_scanner/ScanResult.java5
-rw-r--r--src/main/java/com/it_jaros/network_scanner/Scanner.java148
9 files changed, 470 insertions, 0 deletions
diff --git a/src/main/java/com/it_jaros/network_scanner/App.java b/src/main/java/com/it_jaros/network_scanner/App.java
new file mode 100644
index 0000000..242ae54
--- /dev/null
+++ b/src/main/java/com/it_jaros/network_scanner/App.java
@@ -0,0 +1,36 @@
+package com.it_jaros.network_scanner;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class App {
+ public static void main(String[] args) {
+ CliParser cliParser = new CliParser();
+ try {
+ ScanOptions options = cliParser.parseArgs(args);
+ Scanner scanner = new Scanner(options.openSocketLimit(), options.timeoutInMillis(),
+ options.delayInMillis(), options.maxWorkersPerHost());
+ printResults(scanner.scan(options));
+ } catch (IllegalArgumentException e) {
+ System.out.printf("Error while parsing arguments: %s", e.getMessage());
+ cliParser.usageAndExit();
+ } catch (Exception e) {
+ System.out.printf("ERROR: %s", e.getMessage());
+ }
+ }
+
+ public static void printResults(List<ScanResult> results) {
+ results.forEach(App::printResult);
+ }
+
+ private static void printResult(ScanResult result) {
+ String ports = result.openPorts().stream().mapToObj(String::valueOf).collect(Collectors.joining(","));
+ String output = "[" + result.target() + "]:";
+ if (ports.isEmpty()) {
+ output = output + "No open ports found";
+ } else {
+ output = output + ports;
+ }
+ System.out.print(output + "\n");
+ }
+}
diff --git a/src/main/java/com/it_jaros/network_scanner/CliParser.java b/src/main/java/com/it_jaros/network_scanner/CliParser.java
new file mode 100644
index 0000000..84fa8dc
--- /dev/null
+++ b/src/main/java/com/it_jaros/network_scanner/CliParser.java
@@ -0,0 +1,80 @@
+package com.it_jaros.network_scanner;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class CliParser {
+
+ public ScanOptions parseArgs(String[] args) {
+ if (args.length < 1 || args[0].trim().equals("")) {
+ throw new IllegalArgumentException("No argument given");
+ }
+
+ Set<String> targets = new HashSet<>();
+ int socketLimit = 1000;
+ int delayInMillis = 0;
+ int timeoutInMillis = 1000;
+ int maxWorkersPerHost = 1000;
+ String ports = "1-1024";
+ for (int i = 0; i < args.length; i++) {
+ final String currentArg = args[i];
+ if (!currentArg.startsWith("-")) {
+ targets.add(currentArg);
+ continue;
+ }
+
+ try {
+ switch (currentArg) {
+ case "--help", "-h":
+ usageAndExit();
+ break;
+ case "--socketLimit", "-sl":
+ socketLimit = Integer.parseInt(args[++i]);
+ break;
+ case "--delay", "-d":
+ delayInMillis = Integer.parseInt(args[++i]);
+ break;
+ case "--timeout", "-t":
+ timeoutInMillis = Integer.parseInt(args[++i]);
+ break;
+ case "--maxWorkersPerHost", "-wh":
+ maxWorkersPerHost = Integer.parseInt(args[++i]);
+ break;
+ case "--ports", "-p":
+ ports = args[++i];
+ break;
+ default:
+ throw new IllegalArgumentException("No such param " + currentArg);
+ }
+ } catch (IndexOutOfBoundsException | NumberFormatException e) {
+ System.out.printf("Missing or wrong argument for option %s", currentArg);
+ System.exit(2);
+ }
+ }
+
+ System.out.printf(
+ "Parsed arguments: targets('%s'), ports(%s), socketLimit(%s), delayInMillis(%s), timeoutInMillis(%s), maxWorkersPerHost(%s)%n",
+ targets, ports, socketLimit, delayInMillis, timeoutInMillis, maxWorkersPerHost);
+
+ return new ScanOptions(targets.stream().toList(), socketLimit, delayInMillis, timeoutInMillis, maxWorkersPerHost, ports);
+ }
+
+ public void usageAndExit() {
+ System.err.println("""
+ Usage:
+ java -jar scanner.jar [OPTIONS] <host>
+
+ Options:
+ --delay, -d: Waiting period in millis for connection attempts for a host; (Default: 0)
+ --socketLimit, -sl: Socket Connection limit for a host; (Default: 1000)
+ --timeout, -t: Timeout per connection attempt, 0 means infinite: (Default: 1000)
+ --ports, -p: Ports to be scanned
+ --maxWorkersPerHost, -wh: Virtual Worker Threads run per target. Higher doesn't mean necessarily faster. But the more you use the more RAM is required. (Default: 1000)
+
+ Examples:
+ java -jar scanner.jar localhost
+ java -jar scanner.jar fd00::1
+ """);
+ System.exit(1);
+ }
+}
diff --git a/src/main/java/com/it_jaros/network_scanner/Counter.java b/src/main/java/com/it_jaros/network_scanner/Counter.java
new file mode 100644
index 0000000..f9ff67a
--- /dev/null
+++ b/src/main/java/com/it_jaros/network_scanner/Counter.java
@@ -0,0 +1,25 @@
+package com.it_jaros.network_scanner;
+
+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/network_scanner/PortRange.java b/src/main/java/com/it_jaros/network_scanner/PortRange.java
new file mode 100644
index 0000000..2fd3909
--- /dev/null
+++ b/src/main/java/com/it_jaros/network_scanner/PortRange.java
@@ -0,0 +1,91 @@
+package com.it_jaros.network_scanner;
+
+import java.util.BitSet;
+import java.util.NoSuchElementException;
+
+public class PortRange {
+
+ public static final int MIN_PORT = 1;
+ public static final int MAX_PORT = 65535;
+ private final BitSet specifiedPorts = new BitSet(MAX_PORT);
+ private final BitSet availablePorts = new BitSet(MAX_PORT);
+
+ private int cursor;
+ private int done = 0;
+
+ public PortRange(String ports) {
+ if (ports == null || "".equals(ports)) {
+ // keep defaults
+ specifiedPorts.set(1, 1024 + 1);
+ return;
+ }
+
+ String[] splitComma = ports.split(",");
+ for (String commaValue : splitComma) {
+ if (commaValue.contains("-")) {
+ String[] rangeValue = commaValue.split("-");
+ if (rangeValue.length > 2) {
+ throw new IllegalArgumentException(String.format("Argument contains too many '-'' %s", commaValue));
+ }
+
+ int start = Integer.parseInt(rangeValue[0]);
+ int end = Integer.parseInt(rangeValue[1]);
+ checkValues(start, end);
+ specifiedPorts.set(start, end + 1);
+ } else {
+ int port = Integer.parseInt(commaValue);
+ checkValue(port);
+ specifiedPorts.set(Integer.parseInt(commaValue));
+ }
+ }
+
+ availablePorts.or(specifiedPorts);
+ cursor = availablePorts.nextSetBit(MIN_PORT);
+ }
+
+ private void checkValues(int start, int end) {
+ checkValue(start);
+ checkValue(end);
+
+ if (start >= end) {
+ throw new IllegalArgumentException(
+ String.format("Start value cannot be equal or bigger than end value '%s >= %s'", start, end));
+ }
+ }
+
+ private void checkValue(int port) {
+ if (port < MIN_PORT) {
+ throw new IllegalArgumentException(
+ String.format("Start value smaller than allowed range %s < %s", port, MIN_PORT));
+ }
+
+ if (port > MAX_PORT) {
+ throw new IllegalArgumentException(
+ String.format("End value bigger than allowed range %s > %s", port, MAX_PORT));
+ }
+ }
+
+ public int next() {
+ int p = availablePorts.nextSetBit(cursor);
+ if (p > 0) {
+ availablePorts.clear(p);
+ cursor = p + 1;
+ done++;
+ return p;
+ }
+
+ throw new NoSuchElementException("Reached end of port range");
+ }
+
+ public boolean hasNext() {
+ return availablePorts.nextSetBit(cursor) > 0;
+ }
+
+ public int getTotal() {
+ return specifiedPorts.cardinality();
+ }
+
+ public int getDone() {
+ return done;
+ }
+}
diff --git a/src/main/java/com/it_jaros/network_scanner/Progress.java b/src/main/java/com/it_jaros/network_scanner/Progress.java
new file mode 100644
index 0000000..24ea343
--- /dev/null
+++ b/src/main/java/com/it_jaros/network_scanner/Progress.java
@@ -0,0 +1,5 @@
+package com.it_jaros.network_scanner;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public record Progress(String host, int total, AtomicInteger done, AtomicInteger open) {} \ No newline at end of file
diff --git a/src/main/java/com/it_jaros/network_scanner/ProgressBar.java b/src/main/java/com/it_jaros/network_scanner/ProgressBar.java
new file mode 100644
index 0000000..7743ed1
--- /dev/null
+++ b/src/main/java/com/it_jaros/network_scanner/ProgressBar.java
@@ -0,0 +1,75 @@
+package com.it_jaros.network_scanner;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class ProgressBar {
+
+ private final ScheduledExecutorService ui = Executors.newSingleThreadScheduledExecutor();
+ private final ReentrantLock lock = new ReentrantLock();
+
+ private List<Progress> hosts;
+
+ public void start() {
+ this.hosts = new ArrayList<>();
+ System.out.print("Scanning targets...\n\n");
+ ui.scheduleAtFixedRate(() -> {
+ lock.lock();
+ drawProgress(false);
+ lock.unlock();
+ }, 1, 200, TimeUnit.MILLISECONDS);
+ }
+
+ private void drawProgress(boolean last) {
+ // paint a bar per host
+ for (Progress progress : hosts) {
+ drawBar(progress);
+ }
+
+ if (!last) {
+ // move cursor up again
+ System.out.print("\u001b[" + hosts.size() + "A");
+ }
+ }
+
+ public void submit(Progress progress) {
+ lock.lock();
+ hosts.add(progress);
+ lock.unlock();
+ }
+
+ private void drawBar(Progress progress) {
+ System.out.print("\u001b[2K\r");
+ System.out.println(createBar(
+ progress.host(),
+ progress.open().get(),
+ progress.done().get(),
+ progress.total()));
+ }
+
+ public String createBar(String label, int open, int done, int total) {
+ int width = 30;
+ double pct = total == 0 ? 1.0 : (done / (double) total);
+ int filled = (int) (pct * width);
+
+ String brackets = "[" + "#".repeat(filled) + "-".repeat(width - filled) + "]";
+ int percent = (int) (pct * 100);
+
+ return String.format("%-40s open=%s %s %3d%% (%d/%d)", label, open, brackets, percent, done, total);
+ }
+
+ public void stop() {
+ ui.shutdown();
+ try {
+ ui.awaitTermination(10, TimeUnit.SECONDS);
+ // we need to draw one last time for the 100% to appear
+ drawProgress(true);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
diff --git a/src/main/java/com/it_jaros/network_scanner/ScanOptions.java b/src/main/java/com/it_jaros/network_scanner/ScanOptions.java
new file mode 100644
index 0000000..161c0d8
--- /dev/null
+++ b/src/main/java/com/it_jaros/network_scanner/ScanOptions.java
@@ -0,0 +1,5 @@
+package com.it_jaros.network_scanner;
+
+import java.util.List;
+
+public record ScanOptions(List<String> targets, int openSocketLimit, int delayInMillis, int timeoutInMillis, int maxWorkersPerHost, String ports) {}
diff --git a/src/main/java/com/it_jaros/network_scanner/ScanResult.java b/src/main/java/com/it_jaros/network_scanner/ScanResult.java
new file mode 100644
index 0000000..b7cd8b1
--- /dev/null
+++ b/src/main/java/com/it_jaros/network_scanner/ScanResult.java
@@ -0,0 +1,5 @@
+package com.it_jaros.network_scanner;
+
+import java.util.BitSet;
+
+public record ScanResult(String target, BitSet openPorts) {}
diff --git a/src/main/java/com/it_jaros/network_scanner/Scanner.java b/src/main/java/com/it_jaros/network_scanner/Scanner.java
new file mode 100644
index 0000000..81cbe1b
--- /dev/null
+++ b/src/main/java/com/it_jaros/network_scanner/Scanner.java
@@ -0,0 +1,148 @@
+package com.it_jaros.network_scanner;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
+
+public class Scanner {
+
+ private final Counter counter = new Counter();
+ private final ProgressBar progressBar = new ProgressBar();
+ private final Semaphore socketLimit;
+ private final int timeoutInMillis;
+ private final long delayInNanos;
+ private final int maxWorkersPerHost;
+
+ public Scanner(int socketLimit, int timeoutInMillis, int delayInMillis, int maxWorkersPerHost) {
+ this.timeoutInMillis = timeoutInMillis;
+ this.delayInNanos = TimeUnit.MILLISECONDS.toNanos(Math.max(0, delayInMillis));
+ this.socketLimit = new Semaphore(socketLimit);
+ this.maxWorkersPerHost = maxWorkersPerHost;
+ }
+
+ public List<ScanResult> scan(ScanOptions target) {
+ return scanTargets(target.targets(), target.ports());
+ }
+
+ public List<ScanResult> scanTargets(List<String> targets, String ports) {
+ List<ScanResult> results = new ArrayList<>();
+ progressBar.start();
+ try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+ List<Future<ScanResult>> futures = new ArrayList<>();
+ for (String target : targets) {
+ futures.add(executor.submit(() -> scanTarget(target, ports)));
+ }
+
+ futures.forEach((Future<ScanResult> f) -> {
+ try {
+ results.add(f.get());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException e) {
+ System.out.printf("%s -> %s%n", e.getClass().getSimpleName(), e.getMessage());
+ }
+ });
+ }
+
+ progressBar.stop();
+ System.out.println("Peak concurrent connects: " + counter.max());
+ return results;
+ }
+
+ private ScanResult scanTarget(String target, String ports) {
+ PortRange portRange = new PortRange(ports);
+ AtomicLong portSlotFactory = new AtomicLong(System.nanoTime());
+ Semaphore maxWorkers = new Semaphore(maxWorkersPerHost);
+ BitSet openPorts = new BitSet(PortRange.MAX_PORT);
+
+ Progress progress = new Progress(target, portRange.getTotal(), new AtomicInteger(), new AtomicInteger());
+ progressBar.submit(progress);
+
+ try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+ List<Future<Integer>> futures = new ArrayList<>();
+ while (portRange.hasNext()) {
+ maxWorkers.acquireUninterruptibly();
+ final int currentPort = portRange.next();
+ futures.add(executor.submit(() -> {
+ try {
+ waitForSlot(portSlotFactory);
+ int port = scanPort(target, currentPort);
+ if (port != -1) {
+ progress.open().incrementAndGet();
+ }
+ return port;
+ } finally {
+ maxWorkers.release();
+ progress.done().incrementAndGet();
+ }
+ }));
+ }
+
+ List<Throwable> errors = new ArrayList<>();
+ for (Future<Integer> f : futures) {
+ try {
+ int port = f.get();
+ if (port != -1) {
+ // openPorts is not threadsafe, so we do not update it outside the worker threads
+ openPorts.set(port);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException e) {
+ errors.add(e);
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ System.out.printf("Errors happened during scan of target %s%nErrors:%s -> %s", target, errors.size(), errors);
+ }
+ }
+
+ return new ScanResult(target, openPorts);
+ }
+
+ private int scanPort(String target, final int currentPort) {
+ return isTargetPortOpen(target, currentPort) ? currentPort : -1;
+ }
+
+ private void waitForSlot(AtomicLong scanSlotFactory) {
+ if (delayInNanos > 0) {
+ long slot = scanSlotFactory.getAndAdd(delayInNanos);
+ long wait = slot - System.nanoTime();
+ if (wait > 0)
+ LockSupport.parkNanos(wait);
+ }
+ }
+
+ private boolean isTargetPortOpen(String target, int port) {
+ socketLimit.acquireUninterruptibly();
+ counter.inc();
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress(target, port), timeoutInMillis);
+ return true;
+ } catch (SocketException | SocketTimeoutException ignored) {
+ // Will happen a lot when scanning for open ports, so not needed
+ } catch (IOException e) {
+ System.out.printf("%s -> %s", e.getClass().getSimpleName(), e.getMessage());
+ } finally {
+ counter.dec();
+ socketLimit.release();
+ }
+
+ return false;
+ }
+}