commit 6c2a1042f1653baac87980ca91b0de0e5c390113 parent 6dc652a21d687a89147e564ac4af8f876ca44dbb Author: Matthias Jaros <jaros@mailbox.org> Date: Mon, 16 Mar 2026 16:41:25 +0100 Renamed package Diffstat:
19 files changed, 472 insertions(+), 472 deletions(-)
diff --git a/pom.xml b/pom.xml @@ -69,7 +69,7 @@ <configuration> <archive> <manifest> - <mainClass>com.it_jaros.networkScanner.App</mainClass> + <mainClass>com.it_jaros.network_scanner.App</mainClass> </manifest> </archive> </configuration> diff --git a/src/main/java/com/it_jaros/networkScanner/App.java b/src/main/java/com/it_jaros/networkScanner/App.java @@ -1,36 +0,0 @@ -package com.it_jaros.networkScanner; - -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/networkScanner/CliParser.java b/src/main/java/com/it_jaros/networkScanner/CliParser.java @@ -1,80 +0,0 @@ -package com.it_jaros.networkScanner; - -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/networkScanner/Counter.java b/src/main/java/com/it_jaros/networkScanner/Counter.java @@ -1,25 +0,0 @@ -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/PortRange.java b/src/main/java/com/it_jaros/networkScanner/PortRange.java @@ -1,91 +0,0 @@ -package com.it_jaros.networkScanner; - -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/networkScanner/Progress.java b/src/main/java/com/it_jaros/networkScanner/Progress.java @@ -1,5 +0,0 @@ -package com.it_jaros.networkScanner; - -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/networkScanner/ProgressBar.java b/src/main/java/com/it_jaros/networkScanner/ProgressBar.java @@ -1,75 +0,0 @@ -package com.it_jaros.networkScanner; - -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/networkScanner/ScanOptions.java b/src/main/java/com/it_jaros/networkScanner/ScanOptions.java @@ -1,5 +0,0 @@ -package com.it_jaros.networkScanner; - -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/networkScanner/ScanResult.java b/src/main/java/com/it_jaros/networkScanner/ScanResult.java @@ -1,5 +0,0 @@ -package com.it_jaros.networkScanner; - -import java.util.BitSet; - -public record ScanResult(String target, BitSet openPorts) {} diff --git a/src/main/java/com/it_jaros/networkScanner/Scanner.java b/src/main/java/com/it_jaros/networkScanner/Scanner.java @@ -1,148 +0,0 @@ -package com.it_jaros.networkScanner; - -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; - } -} diff --git a/src/main/java/com/it_jaros/network_scanner/App.java 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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; + } +}