diff options
Diffstat (limited to 'src/main/java/com/it_jaros/network_scanner')
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; + } +} |
