commit f78c37edd54d7ccea53f8d3101832c479aeb1a1c parent 5fcdf11aff7dc463eb4424a18782cb3e4e56dd64 Author: Matthias Jaros <jaros@mailbox.org> Date: Mon, 13 Apr 2026 17:31:55 +0200 Renamed package from network_scanner to jscanner Diffstat:
25 files changed, 641 insertions(+), 642 deletions(-)
diff --git a/pom.xml b/pom.xml @@ -3,10 +3,10 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.it-jaros</groupId> - <artifactId>networkScanner</artifactId> + <artifactId>jscanner</artifactId> <version>1.0-SNAPSHOT</version> - <name>networkScanner</name> + <name>jscanner</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> @@ -68,7 +68,7 @@ <configuration> <archive> <manifest> - <mainClass>com.it_jaros.network_scanner.App</mainClass> + <mainClass>com.it_jaros.jscanner.App</mainClass> </manifest> </archive> </configuration> diff --git a/src/main/java/com/it_jaros/jscanner/App.java b/src/main/java/com/it_jaros/jscanner/App.java @@ -0,0 +1,45 @@ +package com.it_jaros.jscanner; + +import java.util.BitSet; +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(), options.maxHostsLimit(), options.disableHostCheck()); + printResults(scanner.scan(options), options.showFilteredPorts()); + } 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, boolean showFilteredPorts) { + results.forEach((ScanResult r) -> printResult(r, showFilteredPorts)); + } + + private static void printResult(ScanResult result, boolean showFilteredPorts) { + if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) { + return; + } + + System.out.println(result.host()); + if (!result.openPorts().isEmpty()) { + System.out.printf("\t (%d) open:\t%s%n", result.openPorts().cardinality(), map(result.openPorts())); + } + if (showFilteredPorts && !result.filteredPorts().isEmpty()) { + System.out.printf("\t (%d) filtered:\t%s%n", result.filteredPorts().cardinality(), map(result.filteredPorts())); + } + System.out.println(); + } + + private static String map(BitSet ports) { + return ports.stream().mapToObj(String::valueOf).collect(Collectors.joining(",")); + } +} diff --git a/src/main/java/com/it_jaros/jscanner/CliDefaults.java b/src/main/java/com/it_jaros/jscanner/CliDefaults.java @@ -0,0 +1,12 @@ +package com.it_jaros.jscanner; + +public class CliDefaults { + public static final int SOCKET_LIMIT = 1000; + public static final int DELAY_IN_MILLIS = 0; + public static final int TIMEOUT_IN_MILLIS = 300; + public static final int MAX_WORKERS_PER_HOST = 1000; + public static final int MAX_HOSTS_LIMIT = 10; + public static final String PORTS = "1-1024"; + public static final boolean SHOW_FILTERED_PORTS = false; + public static final boolean DISABLE_HOST_CHECK = false; +} diff --git a/src/main/java/com/it_jaros/jscanner/CliParser.java b/src/main/java/com/it_jaros/jscanner/CliParser.java @@ -0,0 +1,112 @@ +package com.it_jaros.jscanner; + +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> hostsArgv = new HashSet<>(); + int socketLimit = CliDefaults.SOCKET_LIMIT; + int delayInMillis = CliDefaults.DELAY_IN_MILLIS; + int timeoutInMillis = CliDefaults.TIMEOUT_IN_MILLIS; + int maxWorkersPerHost = CliDefaults.MAX_WORKERS_PER_HOST; + int maxHostsLimit = CliDefaults.MAX_HOSTS_LIMIT; + String ports = CliDefaults.PORTS; + boolean showFilteredPorts = CliDefaults.SHOW_FILTERED_PORTS; + boolean disableHostCheck = CliDefaults.DISABLE_HOST_CHECK; + String hostsFile = null; + + for (int i = 0; i < args.length; i++) { + final String currentArg = args[i]; + if (!currentArg.startsWith("-")) { + hostsArgv.add(currentArg); + continue; + } + + switch (currentArg) { + case "--help", "-h": + usageAndExit(); + break; + case "--socketLimit", "-sl": + socketLimit = parseIntOption(args, ++i, currentArg); + break; + case "--delay", "-d": + delayInMillis = parseIntOption(args, ++i, currentArg); + break; + case "--timeout", "-t": + timeoutInMillis = parseIntOption(args, ++i, currentArg); + break; + case "--maxWorkersPerHost", "-wh": + maxWorkersPerHost = parseIntOption(args, ++i, currentArg); + break; + case "--maxHostsLimit", "-tl": + maxHostsLimit = parseIntOption(args, ++i, currentArg); + break; + case "--ports", "-p": + ports = requireValue(args, ++i, currentArg); + break; + case "--showFilteredPorts", "-sf": + showFilteredPorts = true; + break; + case "--disableHostCheck", "-dt": + disableHostCheck = true; + break; + case "--file", "-f": + hostsFile = requireValue(args, ++i, currentArg); + break; + default: + throw new IllegalArgumentException("No such param " + currentArg); + } + } + + System.out.printf( + "Parsed arguments: hostsArgv('%s'), ports(%s), socketLimit(%s), delayInMillis(%s), timeoutInMillis(%s), maxWorkersPerHost(%s), maxTargetsLimit(%s), showFilteredPorts(%s), disableTargetCheck(%s), file(%s) %n", + hostsArgv, ports, socketLimit, delayInMillis, timeoutInMillis, maxWorkersPerHost, maxHostsLimit, showFilteredPorts, disableHostCheck, hostsFile); + + return new ScanOptions(hostsArgv.stream().toList(), socketLimit, delayInMillis, timeoutInMillis, maxWorkersPerHost, maxHostsLimit, ports, showFilteredPorts, disableHostCheck, hostsFile); + } + + private String requireValue(String[] args, int index, String option) { + if (index >= args.length) { + throw new IllegalArgumentException(String.format("Missing argument for option %s", option)); + } + return args[index]; + } + + private int parseIntOption(String[] args, int index, String option) { + String value = requireValue(args, index, option); + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(String.format("Invalid argument %s for option %s", value, option)); + } + } + + public void usageAndExit() { + System.err.println(""" + Usage: + java -jar scanner.jar [OPTIONS] <host> + + Options: + --delay, -d:\t\tWaiting period in millis for connection attempts for a host (Default: 0) + --socketLimit, -sl:\t\tSocket Connection limit for a host (Default: 1000) + --timeout, -t:\t\tTimeout per connection attempt, 0 means infinite (Default: 1000) + --ports, -p:\t\tPorts to be scanned + --maxWorkersPerHost, -wh:\tVirtual Worker Threads run per Host. Higher doesn't mean necessarily faster. But the more you use the more RAM is required (Default: 1000) + --maxHostsLimit, -tl:\tMax number of Hosts that are scanned at the same time (Default: 10) + --showFilteredPorts, -sf:\tShow also filtered ports + --disableHostCheck, -dt:\tDon't check if Host is reachable + --file, -f:\t\t\tRead hosts to scan from file instead of stdin + + Examples: + java -jar scanner.jar localhost + java -jar scanner.jar fd00::1 + """); + System.exit(1); + } +} diff --git a/src/main/java/com/it_jaros/jscanner/Counter.java b/src/main/java/com/it_jaros/jscanner/Counter.java @@ -0,0 +1,25 @@ +package com.it_jaros.jscanner; + +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/jscanner/PortRange.java b/src/main/java/com/it_jaros/jscanner/PortRange.java @@ -0,0 +1,91 @@ +package com.it_jaros.jscanner; + +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/jscanner/PortResult.java b/src/main/java/com/it_jaros/jscanner/PortResult.java @@ -0,0 +1,4 @@ +package com.it_jaros.jscanner; + +public record PortResult(int port, PortState state) { +} diff --git a/src/main/java/com/it_jaros/jscanner/PortState.java b/src/main/java/com/it_jaros/jscanner/PortState.java @@ -0,0 +1,7 @@ +package com.it_jaros.jscanner; + +public enum PortState { + OPEN, + CLOSED, + FILTERED +} diff --git a/src/main/java/com/it_jaros/jscanner/Progress.java b/src/main/java/com/it_jaros/jscanner/Progress.java @@ -0,0 +1,5 @@ +package com.it_jaros.jscanner; + +import java.util.concurrent.atomic.AtomicInteger; + +public record Progress(String host, int total, AtomicInteger done, AtomicInteger open, AtomicInteger filtered) {} diff --git a/src/main/java/com/it_jaros/jscanner/ProgressBar.java b/src/main/java/com/it_jaros/jscanner/ProgressBar.java @@ -0,0 +1,118 @@ +package com.it_jaros.jscanner; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +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; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ProgressBar { + + private final ScheduledExecutorService ui = Executors.newSingleThreadScheduledExecutor(); + private final ReentrantLock lock = new ReentrantLock(); + private final int columnWidth = ProgressBar.getColumnWidth(); + + private List<Progress> hosts; + + private static int getColumnWidth() { + try { + Process process = new ProcessBuilder("sh", "-c", "stty size < /dev/tty").start(); + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line = reader.readLine(); + if (line != null && !line.isBlank()) { + Matcher matcher = Pattern.compile("^\\d+\\s+(\\d+)").matcher(line.trim()); + if (matcher.matches()) { + return Integer.parseInt(matcher.group(1)); + } + } + } + } catch (Exception ignored) { + // does not matter why it did not work + } + + return 120; + } + + 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) { + // find and print finished hosts one last time + for (Iterator<Progress> it = hosts.iterator(); it.hasNext();) { + Progress p = it.next(); + if (p.done().get() == p.total()) { + it.remove(); + drawBar(p); + } + } + + // the more progressed the host the more up it is + hosts.sort(Comparator.comparingInt((Progress p) -> p.done().get()).reversed()); + 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"); // clear whole line + System.out.println(createBar( + progress.host(), + progress.open().get(), + progress.filtered().get(), + progress.done().get(), + progress.total())); + } + + public String createBar(String label, int open, int filtered, int done, int total) { + int terminalWidth = columnWidth; + int bracketWidth = terminalWidth / 3; + double pct = total == 0 ? 1.0 : (done / (double) total); + int percent = (int) (pct * 100); + + String statsLeft = String.format("open:%d filtered:%d (%d/%d)", open, filtered, done, total); + String statsRight = String.format("%3d%%", percent); + int filled = (int) (pct * bracketWidth); + String brackets = "[" + "#".repeat(filled) + "-".repeat(bracketWidth - filled) + "]"; + + int spacesBetween = terminalWidth - bracketWidth - (label + statsLeft + statsRight).length() - 5; + return String.format("%s%s%s %s %s", label, " ".repeat(spacesBetween),statsLeft, brackets, statsRight); + } + + 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/jscanner/ScanOptions.java b/src/main/java/com/it_jaros/jscanner/ScanOptions.java @@ -0,0 +1,5 @@ +package com.it_jaros.jscanner; + +import java.util.List; + +public record ScanOptions(List<String> hostsArgv, int openSocketLimit, int delayInMillis, int timeoutInMillis, int maxWorkersPerHost, int maxHostsLimit, String ports, boolean showFilteredPorts, boolean disableHostCheck, String hostsFile) {} diff --git a/src/main/java/com/it_jaros/jscanner/ScanResult.java b/src/main/java/com/it_jaros/jscanner/ScanResult.java @@ -0,0 +1,5 @@ +package com.it_jaros.jscanner; + +import java.util.BitSet; + +public record ScanResult(String host, BitSet openPorts, BitSet filteredPorts) {} diff --git a/src/main/java/com/it_jaros/jscanner/Scanner.java b/src/main/java/com/it_jaros/jscanner/Scanner.java @@ -0,0 +1,209 @@ +package com.it_jaros.jscanner; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.*; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.LockSupport; +import java.util.stream.Stream; + +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; + private final int maxHostsLimit; + private final boolean disableHostCheck; + + public Scanner(int socketLimit, int timeoutInMillis, int delayInMillis, int maxWorkersPerHost, int maxHostsLimit, boolean disableHostCheck) { + this.timeoutInMillis = timeoutInMillis; + this.delayInNanos = TimeUnit.MILLISECONDS.toNanos(Math.max(0, delayInMillis)); + this.socketLimit = new Semaphore(socketLimit); + this.maxWorkersPerHost = maxWorkersPerHost; + this.maxHostsLimit = maxHostsLimit; + this.disableHostCheck = disableHostCheck; + } + + public List<ScanResult> scan(ScanOptions options) { + if (options.hostsFile() != null) { + return scanHosts(options.hostsFile(), options.ports()); + } + return scanHosts(options.hostsArgv(), options.ports()); + } + + public List<ScanResult> scanHosts(String sourceFile, String ports) { + try (BufferedReader reader = Files.newBufferedReader(Path.of(sourceFile))) { + return scanHosts(reader.lines(), ports); + } catch (FileNotFoundException | NoSuchFileException e) { + System.out.printf("Could not open file %s%n", sourceFile); + } catch (IOException e) { + System.out.printf("Error while trying to read hosts: %s -> %s %n", e.getClass().getSimpleName(), e.getMessage()); + } + + return List.of(); + } + + public List<ScanResult> scanHosts(List<String> hostsArgv, String ports) { + try (Stream<String> stream = hostsArgv.stream()){ + return scanHosts(stream, ports); + } catch (IOException e) { + System.out.printf("Error while trying to read hosts: %s -> %s %n", e.getClass().getSimpleName(), e.getMessage()); + } + + return List.of(); + } + + public List<ScanResult> scanHosts(Stream<String> hosts, String ports) throws IOException { + List<ScanResult> results = new ArrayList<>(); + progressBar.start(); + try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { + List<Future<ScanResult>> futures = new ArrayList<>(); + Semaphore maxHosts = new Semaphore(maxHostsLimit); + hosts.forEach((final String host) -> { + maxHosts.acquireUninterruptibly(); + futures.add(executor.submit(() -> { + try { + return scanHost(host, ports); + } finally { + maxHosts.release(); + } + })); + }); + + 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("--------------------"); + System.out.println("Finished scan!"); + System.out.println("Stats:"); + System.out.println("Peak concurrent connects: " + counter.max()); + System.out.println("--------------------"); + return results; + } + + private ScanResult scanHost(String host, 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); + BitSet filteredPorts = new BitSet(PortRange.MAX_PORT); + + Progress progress = new Progress(host, portRange.getTotal(), new AtomicInteger(), new AtomicInteger(), new AtomicInteger()); + progressBar.submit(progress); + + if(!disableHostCheck && !checkHostOnline(host)) { + // Visually show that this host is basically done + progress.done().set(progress.total()); + return new ScanResult(host, openPorts, filteredPorts); + } + + try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { + List<Future<PortResult>> futures = new ArrayList<>(); + while (portRange.hasNext()) { + maxWorkers.acquireUninterruptibly(); + final int currentPort = portRange.next(); + futures.add(executor.submit(() -> { + try { + waitForSlot(portSlotFactory); + PortState state = getPortState(host, currentPort); + if (state.equals(PortState.OPEN)) { + progress.open().incrementAndGet(); + } else if (state.equals(PortState.FILTERED)) { + progress.filtered().incrementAndGet(); + } + return new PortResult(currentPort, state); + } finally { + maxWorkers.release(); + progress.done().incrementAndGet(); + } + })); + } + + List<Throwable> errors = new ArrayList<>(); + for (Future<PortResult> f : futures) { + try { + // bitset is not thread-safe, so it is set + // outside the other virtual threads that update progress + PortResult portResult = f.get(); + if (portResult.state().equals(PortState.OPEN)) { + openPorts.set(portResult.port()); + } else if (portResult.state().equals(PortState.FILTERED)) { + filteredPorts.set(portResult.port()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + errors.add(e); + } + } + + if (!errors.isEmpty()) { + System.out.printf("Errors happened during scan of host %s%nErrors:%s -> %s", host, errors.size(), errors); + } + } + + return new ScanResult(host, openPorts, filteredPorts); + } + + private boolean checkHostOnline(String host) { + try { + return InetAddress.getByName(host).isReachable(timeoutInMillis); + } catch (IOException e) { + // something went wrong + } + + return false; + } + + 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 PortState getPortState(String host, int port) { + socketLimit.acquireUninterruptibly(); + counter.inc(); + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(host, port), timeoutInMillis); + return PortState.OPEN; + } catch (NoRouteToHostException ignored) { + // this can be safely ignored because the port is closed if a host is unreachable + } catch (SocketTimeoutException ignored) { + return PortState.FILTERED; + } catch (ConnectException ignored) { + return PortState.CLOSED; + } catch (IOException ignored) { + // Will happen a lot when scanning for open ports, so not needed + } finally { + counter.dec(); + socketLimit.release(); + } + + return PortState.CLOSED; + } +} diff --git a/src/main/java/com/it_jaros/network_scanner/App.java b/src/main/java/com/it_jaros/network_scanner/App.java @@ -1,45 +0,0 @@ -package com.it_jaros.network_scanner; - -import java.util.BitSet; -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(), options.maxHostsLimit(), options.disableHostCheck()); - printResults(scanner.scan(options), options.showFilteredPorts()); - } 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, boolean showFilteredPorts) { - results.forEach((ScanResult r) -> printResult(r, showFilteredPorts)); - } - - private static void printResult(ScanResult result, boolean showFilteredPorts) { - if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) { - return; - } - - System.out.println(result.host()); - if (!result.openPorts().isEmpty()) { - System.out.printf("\t (%d) open:\t%s%n", result.openPorts().cardinality(), map(result.openPorts())); - } - if (showFilteredPorts && !result.filteredPorts().isEmpty()) { - System.out.printf("\t (%d) filtered:\t%s%n", result.filteredPorts().cardinality(), map(result.filteredPorts())); - } - System.out.println(); - } - - private static String map(BitSet ports) { - return ports.stream().mapToObj(String::valueOf).collect(Collectors.joining(",")); - } -} diff --git a/src/main/java/com/it_jaros/network_scanner/CliDefaults.java b/src/main/java/com/it_jaros/network_scanner/CliDefaults.java @@ -1,12 +0,0 @@ -package com.it_jaros.network_scanner; - -public class CliDefaults { - public static final int SOCKET_LIMIT = 1000; - public static final int DELAY_IN_MILLIS = 0; - public static final int TIMEOUT_IN_MILLIS = 300; - public static final int MAX_WORKERS_PER_HOST = 1000; - public static final int MAX_HOSTS_LIMIT = 10; - public static final String PORTS = "1-1024"; - public static final boolean SHOW_FILTERED_PORTS = false; - public static final boolean DISABLE_HOST_CHECK = false; -} diff --git a/src/main/java/com/it_jaros/network_scanner/CliParser.java b/src/main/java/com/it_jaros/network_scanner/CliParser.java @@ -1,112 +0,0 @@ -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> hostsArgv = new HashSet<>(); - int socketLimit = CliDefaults.SOCKET_LIMIT; - int delayInMillis = CliDefaults.DELAY_IN_MILLIS; - int timeoutInMillis = CliDefaults.TIMEOUT_IN_MILLIS; - int maxWorkersPerHost = CliDefaults.MAX_WORKERS_PER_HOST; - int maxHostsLimit = CliDefaults.MAX_HOSTS_LIMIT; - String ports = CliDefaults.PORTS; - boolean showFilteredPorts = CliDefaults.SHOW_FILTERED_PORTS; - boolean disableHostCheck = CliDefaults.DISABLE_HOST_CHECK; - String hostsFile = null; - - for (int i = 0; i < args.length; i++) { - final String currentArg = args[i]; - if (!currentArg.startsWith("-")) { - hostsArgv.add(currentArg); - continue; - } - - switch (currentArg) { - case "--help", "-h": - usageAndExit(); - break; - case "--socketLimit", "-sl": - socketLimit = parseIntOption(args, ++i, currentArg); - break; - case "--delay", "-d": - delayInMillis = parseIntOption(args, ++i, currentArg); - break; - case "--timeout", "-t": - timeoutInMillis = parseIntOption(args, ++i, currentArg); - break; - case "--maxWorkersPerHost", "-wh": - maxWorkersPerHost = parseIntOption(args, ++i, currentArg); - break; - case "--maxHostsLimit", "-tl": - maxHostsLimit = parseIntOption(args, ++i, currentArg); - break; - case "--ports", "-p": - ports = requireValue(args, ++i, currentArg); - break; - case "--showFilteredPorts", "-sf": - showFilteredPorts = true; - break; - case "--disableHostCheck", "-dt": - disableHostCheck = true; - break; - case "--file", "-f": - hostsFile = requireValue(args, ++i, currentArg); - break; - default: - throw new IllegalArgumentException("No such param " + currentArg); - } - } - - System.out.printf( - "Parsed arguments: hostsArgv('%s'), ports(%s), socketLimit(%s), delayInMillis(%s), timeoutInMillis(%s), maxWorkersPerHost(%s), maxTargetsLimit(%s), showFilteredPorts(%s), disableTargetCheck(%s), file(%s) %n", - hostsArgv, ports, socketLimit, delayInMillis, timeoutInMillis, maxWorkersPerHost, maxHostsLimit, showFilteredPorts, disableHostCheck, hostsFile); - - return new ScanOptions(hostsArgv.stream().toList(), socketLimit, delayInMillis, timeoutInMillis, maxWorkersPerHost, maxHostsLimit, ports, showFilteredPorts, disableHostCheck, hostsFile); - } - - private String requireValue(String[] args, int index, String option) { - if (index >= args.length) { - throw new IllegalArgumentException(String.format("Missing argument for option %s", option)); - } - return args[index]; - } - - private int parseIntOption(String[] args, int index, String option) { - String value = requireValue(args, index, option); - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException(String.format("Invalid argument %s for option %s", value, option)); - } - } - - public void usageAndExit() { - System.err.println(""" - Usage: - java -jar scanner.jar [OPTIONS] <host> - - Options: - --delay, -d:\t\tWaiting period in millis for connection attempts for a host (Default: 0) - --socketLimit, -sl:\t\tSocket Connection limit for a host (Default: 1000) - --timeout, -t:\t\tTimeout per connection attempt, 0 means infinite (Default: 1000) - --ports, -p:\t\tPorts to be scanned - --maxWorkersPerHost, -wh:\tVirtual Worker Threads run per Host. Higher doesn't mean necessarily faster. But the more you use the more RAM is required (Default: 1000) - --maxHostsLimit, -tl:\tMax number of Hosts that are scanned at the same time (Default: 10) - --showFilteredPorts, -sf:\tShow also filtered ports - --disableHostCheck, -dt:\tDon't check if Host is reachable - --file, -f:\t\t\tRead hosts to scan from file instead of stdin - - 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 @@ -1,25 +0,0 @@ -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 @@ -1,91 +0,0 @@ -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/PortResult.java b/src/main/java/com/it_jaros/network_scanner/PortResult.java @@ -1,4 +0,0 @@ -package com.it_jaros.network_scanner; - -public record PortResult(int port, PortState state) { -} diff --git a/src/main/java/com/it_jaros/network_scanner/PortState.java b/src/main/java/com/it_jaros/network_scanner/PortState.java @@ -1,7 +0,0 @@ -package com.it_jaros.network_scanner; - -public enum PortState { - OPEN, - CLOSED, - FILTERED -} diff --git a/src/main/java/com/it_jaros/network_scanner/Progress.java b/src/main/java/com/it_jaros/network_scanner/Progress.java @@ -1,5 +0,0 @@ -package com.it_jaros.network_scanner; - -import java.util.concurrent.atomic.AtomicInteger; - -public record Progress(String host, int total, AtomicInteger done, AtomicInteger open, AtomicInteger filtered) {} -\ 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 @@ -1,118 +0,0 @@ -package com.it_jaros.network_scanner; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.Iterator; -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; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class ProgressBar { - - private final ScheduledExecutorService ui = Executors.newSingleThreadScheduledExecutor(); - private final ReentrantLock lock = new ReentrantLock(); - private final int columnWidth = ProgressBar.getColumnWidth(); - - private List<Progress> hosts; - - private static int getColumnWidth() { - try { - Process process = new ProcessBuilder("sh", "-c", "stty size < /dev/tty").start(); - - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()))) { - String line = reader.readLine(); - if (line != null && !line.isBlank()) { - Matcher matcher = Pattern.compile("^\\d+\\s+(\\d+)").matcher(line.trim()); - if (matcher.matches()) { - return Integer.parseInt(matcher.group(1)); - } - } - } - } catch (Exception ignored) { - // does not matter why it did not work - } - - return 120; - } - - 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) { - // find and print finished hosts one last time - for (Iterator<Progress> it = hosts.iterator(); it.hasNext();) { - Progress p = it.next(); - if (p.done().get() == p.total()) { - it.remove(); - drawBar(p); - } - } - - // the more progressed the host the more up it is - hosts.sort(Comparator.comparingInt((Progress p) -> p.done().get()).reversed()); - 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"); // clear whole line - System.out.println(createBar( - progress.host(), - progress.open().get(), - progress.filtered().get(), - progress.done().get(), - progress.total())); - } - - public String createBar(String label, int open, int filtered, int done, int total) { - int terminalWidth = columnWidth; - int bracketWidth = terminalWidth / 3; - double pct = total == 0 ? 1.0 : (done / (double) total); - int percent = (int) (pct * 100); - - String statsLeft = String.format("open:%d filtered:%d (%d/%d)", open, filtered, done, total); - String statsRight = String.format("%3d%%", percent); - int filled = (int) (pct * bracketWidth); - String brackets = "[" + "#".repeat(filled) + "-".repeat(bracketWidth - filled) + "]"; - - int spacesBetween = terminalWidth - bracketWidth - (label + statsLeft + statsRight).length() - 5; - return String.format("%s%s%s %s %s", label, " ".repeat(spacesBetween),statsLeft, brackets, statsRight); - } - - 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 @@ -1,5 +0,0 @@ -package com.it_jaros.network_scanner; - -import java.util.List; - -public record ScanOptions(List<String> hostsArgv, int openSocketLimit, int delayInMillis, int timeoutInMillis, int maxWorkersPerHost, int maxHostsLimit, String ports, boolean showFilteredPorts, boolean disableHostCheck, String hostsFile) {} diff --git a/src/main/java/com/it_jaros/network_scanner/ScanResult.java b/src/main/java/com/it_jaros/network_scanner/ScanResult.java @@ -1,5 +0,0 @@ -package com.it_jaros.network_scanner; - -import java.util.BitSet; - -public record ScanResult(String host, BitSet openPorts, BitSet filteredPorts) {} diff --git a/src/main/java/com/it_jaros/network_scanner/Scanner.java b/src/main/java/com/it_jaros/network_scanner/Scanner.java @@ -1,209 +0,0 @@ -package com.it_jaros.network_scanner; - -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.*; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.BitSet; -import java.util.List; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.LockSupport; -import java.util.stream.Stream; - -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; - private final int maxHostsLimit; - private final boolean disableHostCheck; - - public Scanner(int socketLimit, int timeoutInMillis, int delayInMillis, int maxWorkersPerHost, int maxHostsLimit, boolean disableHostCheck) { - this.timeoutInMillis = timeoutInMillis; - this.delayInNanos = TimeUnit.MILLISECONDS.toNanos(Math.max(0, delayInMillis)); - this.socketLimit = new Semaphore(socketLimit); - this.maxWorkersPerHost = maxWorkersPerHost; - this.maxHostsLimit = maxHostsLimit; - this.disableHostCheck = disableHostCheck; - } - - public List<ScanResult> scan(ScanOptions options) { - if (options.hostsFile() != null) { - return scanHosts(options.hostsFile(), options.ports()); - } - return scanHosts(options.hostsArgv(), options.ports()); - } - - public List<ScanResult> scanHosts(String sourceFile, String ports) { - try (BufferedReader reader = Files.newBufferedReader(Path.of(sourceFile))) { - return scanHosts(reader.lines(), ports); - } catch (FileNotFoundException | NoSuchFileException e) { - System.out.printf("Could not open file %s%n", sourceFile); - } catch (IOException e) { - System.out.printf("Error while trying to read hosts: %s -> %s %n", e.getClass().getSimpleName(), e.getMessage()); - } - - return List.of(); - } - - public List<ScanResult> scanHosts(List<String> hostsArgv, String ports) { - try (Stream<String> stream = hostsArgv.stream()){ - return scanHosts(stream, ports); - } catch (IOException e) { - System.out.printf("Error while trying to read hosts: %s -> %s %n", e.getClass().getSimpleName(), e.getMessage()); - } - - return List.of(); - } - - public List<ScanResult> scanHosts(Stream<String> hosts, String ports) throws IOException { - List<ScanResult> results = new ArrayList<>(); - progressBar.start(); - try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { - List<Future<ScanResult>> futures = new ArrayList<>(); - Semaphore maxHosts = new Semaphore(maxHostsLimit); - hosts.forEach((final String host) -> { - maxHosts.acquireUninterruptibly(); - futures.add(executor.submit(() -> { - try { - return scanHost(host, ports); - } finally { - maxHosts.release(); - } - })); - }); - - 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("--------------------"); - System.out.println("Finished scan!"); - System.out.println("Stats:"); - System.out.println("Peak concurrent connects: " + counter.max()); - System.out.println("--------------------"); - return results; - } - - private ScanResult scanHost(String host, 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); - BitSet filteredPorts = new BitSet(PortRange.MAX_PORT); - - Progress progress = new Progress(host, portRange.getTotal(), new AtomicInteger(), new AtomicInteger(), new AtomicInteger()); - progressBar.submit(progress); - - if(!disableHostCheck && !checkHostOnline(host)) { - // Visually show that this host is basically done - progress.done().set(progress.total()); - return new ScanResult(host, openPorts, filteredPorts); - } - - try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { - List<Future<PortResult>> futures = new ArrayList<>(); - while (portRange.hasNext()) { - maxWorkers.acquireUninterruptibly(); - final int currentPort = portRange.next(); - futures.add(executor.submit(() -> { - try { - waitForSlot(portSlotFactory); - PortState state = getPortState(host, currentPort); - if (state.equals(PortState.OPEN)) { - progress.open().incrementAndGet(); - } else if (state.equals(PortState.FILTERED)) { - progress.filtered().incrementAndGet(); - } - return new PortResult(currentPort, state); - } finally { - maxWorkers.release(); - progress.done().incrementAndGet(); - } - })); - } - - List<Throwable> errors = new ArrayList<>(); - for (Future<PortResult> f : futures) { - try { - // bitset is not thread-safe, so it is set - // outside the other virtual threads that update progress - PortResult portResult = f.get(); - if (portResult.state().equals(PortState.OPEN)) { - openPorts.set(portResult.port()); - } else if (portResult.state().equals(PortState.FILTERED)) { - filteredPorts.set(portResult.port()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - errors.add(e); - } - } - - if (!errors.isEmpty()) { - System.out.printf("Errors happened during scan of host %s%nErrors:%s -> %s", host, errors.size(), errors); - } - } - - return new ScanResult(host, openPorts, filteredPorts); - } - - private boolean checkHostOnline(String host) { - try { - return InetAddress.getByName(host).isReachable(timeoutInMillis); - } catch (IOException e) { - // something went wrong - } - - return false; - } - - 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 PortState getPortState(String host, int port) { - socketLimit.acquireUninterruptibly(); - counter.inc(); - try (Socket socket = new Socket()) { - socket.connect(new InetSocketAddress(host, port), timeoutInMillis); - return PortState.OPEN; - } catch (NoRouteToHostException ignored) { - // this can be safely ignored because the port is closed if a host is unreachable - } catch (SocketTimeoutException ignored) { - return PortState.FILTERED; - } catch (ConnectException ignored) { - return PortState.CLOSED; - } catch (IOException ignored) { - // Will happen a lot when scanning for open ports, so not needed - } finally { - counter.dec(); - socketLimit.release(); - } - - return PortState.CLOSED; - } -}