summaryrefslogtreecommitdiff
path: root/src/main/java/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com')
-rw-r--r--src/main/java/com/it_jaros/jns/CliDefaults.java1
-rw-r--r--src/main/java/com/it_jaros/jns/CliParser.java5
-rw-r--r--src/main/java/com/it_jaros/jns/scan/ScanOptions.java1
-rw-r--r--src/main/java/com/it_jaros/jns/scan/engine/ScanHostTask.java7
-rw-r--r--src/main/java/com/it_jaros/jns/scan/engine/ScanUdpPortTask.java105
-rw-r--r--src/main/java/com/it_jaros/jns/scan/engine/platform/NativeCallException.java7
-rw-r--r--src/main/java/com/it_jaros/jns/scan/engine/platform/linux/LinuxUdpSocketAPI.java210
-rw-r--r--src/main/java/com/it_jaros/jns/scan/engine/platform/linux/RawLinuxSocketException.java11
8 files changed, 346 insertions, 1 deletions
diff --git a/src/main/java/com/it_jaros/jns/CliDefaults.java b/src/main/java/com/it_jaros/jns/CliDefaults.java
index e000fc9..328e485 100644
--- a/src/main/java/com/it_jaros/jns/CliDefaults.java
+++ b/src/main/java/com/it_jaros/jns/CliDefaults.java
@@ -10,6 +10,7 @@ public class CliDefaults {
public static final boolean DISABLE_HOST_CHECK = false;
public static final boolean QUIET = false;
public static final boolean SHOW_FILTERED_PORTS = false;
+ public static final boolean USE_UDP = false;
public static final int DELAY_IN_MILLIS = 0;
public static final int MAX_HOSTS_LIMIT = 100;
public static final int MAX_WORKERS_PER_HOST = Integer.MAX_VALUE;
diff --git a/src/main/java/com/it_jaros/jns/CliParser.java b/src/main/java/com/it_jaros/jns/CliParser.java
index 0a3dcc5..cf4f863 100644
--- a/src/main/java/com/it_jaros/jns/CliParser.java
+++ b/src/main/java/com/it_jaros/jns/CliParser.java
@@ -22,6 +22,7 @@ public class CliParser {
boolean help = false;
boolean quiet = CliDefaults.QUIET;
boolean showFilteredPorts = CliDefaults.SHOW_FILTERED_PORTS;
+ boolean useUdp = CliDefaults.USE_UDP;
int delayInMillis = CliDefaults.DELAY_IN_MILLIS;
int hostsLimit = CliDefaults.MAX_HOSTS_LIMIT;
int socketLimit = CliDefaults.SOCKET_LIMIT;
@@ -66,6 +67,9 @@ public class CliParser {
case "--showFilteredPorts", "-sf":
showFilteredPorts = true;
break;
+ case "--useUdp", "-uu":
+ useUdp = true;
+ break;
case "--socketLimit", "-sl":
socketLimit = parseIntOption(args, ++i, currentArg, (Integer value) -> value > 0);
break;
@@ -89,6 +93,7 @@ public class CliParser {
disableOnlineCheck,
quiet,
showFilteredPorts,
+ useUdp,
delayInMillis,
hostsLimit,
workersPerHostLimit,
diff --git a/src/main/java/com/it_jaros/jns/scan/ScanOptions.java b/src/main/java/com/it_jaros/jns/scan/ScanOptions.java
index d6035c7..f2e0222 100644
--- a/src/main/java/com/it_jaros/jns/scan/ScanOptions.java
+++ b/src/main/java/com/it_jaros/jns/scan/ScanOptions.java
@@ -10,6 +10,7 @@ public record ScanOptions(
boolean disableOnlineCheck,
boolean quiet,
boolean showFilteredPorts,
+ boolean useUdp,
int delayInMillis,
int maxHostsLimit,
int maxWorkersPerHost,
diff --git a/src/main/java/com/it_jaros/jns/scan/engine/ScanHostTask.java b/src/main/java/com/it_jaros/jns/scan/engine/ScanHostTask.java
index 7fee311..9a4d6db 100644
--- a/src/main/java/com/it_jaros/jns/scan/engine/ScanHostTask.java
+++ b/src/main/java/com/it_jaros/jns/scan/engine/ScanHostTask.java
@@ -9,6 +9,7 @@ import com.it_jaros.jns.scan.domain.ScanResult;
import java.net.InetAddress;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
public class ScanHostTask implements Callable<ScanResult> {
@@ -64,11 +65,15 @@ public class ScanHostTask implements Callable<ScanResult> {
}
final PortRange portRange = new PortRange(scan.getPorts());
+ Function<Integer, Callable<PortResult>> scanFactory = this.context.scanOptions().useUdp()
+ ? port -> new ScanUdpPortTask(port, context, hostContext)
+ : port -> new ScanPortTask(port, context, hostContext);
+
// producer thread
ProducerState<PortResult> state = new ProducerThread(context).startProducer(
portRange.iterator(),
maxWorkersPerHost,
- port -> new ScanPortTask(port, context, hostContext)
+ scanFactory
);
new CompletionConsumer<>(cancelledToken, state).runConsumerLoop(poll -> {
diff --git a/src/main/java/com/it_jaros/jns/scan/engine/ScanUdpPortTask.java b/src/main/java/com/it_jaros/jns/scan/engine/ScanUdpPortTask.java
new file mode 100644
index 0000000..21b93f8
--- /dev/null
+++ b/src/main/java/com/it_jaros/jns/scan/engine/ScanUdpPortTask.java
@@ -0,0 +1,105 @@
+package com.it_jaros.jns.scan.engine;
+
+import com.it_jaros.jns.scan.Scan;
+import com.it_jaros.jns.scan.domain.PortResult;
+import com.it_jaros.jns.scan.domain.PortState;
+import com.it_jaros.jns.scan.engine.platform.linux.LinuxUdpSocketAPI;
+import com.it_jaros.jns.scan.engine.platform.linux.RawLinuxSocketException;
+
+import java.io.IOException;
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.net.InetAddress;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Semaphore;
+
+public class ScanUdpPortTask implements Callable<PortResult> {
+
+ private static final int RECV_BUFFER_SIZE = 1024;
+
+ private final CancelledToken cancelledToken;
+ private final InetAddress inetAddress;
+ private final PortScanRateLimiter portScanRateLimiter; // per-host shared limiter
+ private final Scan scan;
+ private final Semaphore socketLimit;
+ private final int port;
+ private final long timeoutInMillis;
+
+ ScanUdpPortTask(int port, ScanExecutionContext context, ScanHostContext hostContext) {
+ this.inetAddress = hostContext.hostAddress();
+ this.port = port;
+ this.cancelledToken = context.cancelledToken();
+ this.portScanRateLimiter = hostContext.portScanRateLimiter();
+ this.scan = context.scan();
+ this.socketLimit = context.socketLimit();
+ this.timeoutInMillis = context.scanOptions().timeoutInMillis();
+ }
+
+ @Override
+ public PortResult call() throws Exception {
+ try {
+ socketLimit.acquire();
+ try {
+ scan.portStart();
+ portScanRateLimiter.apply();
+ if (cancelledToken.isCancelled()) {
+ return PortResult.empty();
+ }
+ return checkPort();
+ } finally {
+ scan.portFinish();
+ socketLimit.release();
+ }
+ } catch (Exception e) {
+ throw new ScanPortException(port, e);
+ }
+ }
+
+ private PortResult checkPort() {
+ PortResult result = new PortResult();
+ result.setPort(port);
+ result.setState(PortState.UNKNOWN);
+ try {
+ PortState portState = probeUdpPort();
+ result.setState(portState);
+ } catch (IOException e) {
+ result.setException(e);
+ }
+ return result;
+ }
+
+ private PortState probeUdpPort() throws IOException {
+ try (Arena arena = Arena.ofConfined()) {
+ LinuxUdpSocketAPI linuxUdpSocketAPI = new LinuxUdpSocketAPI(arena);
+ MemorySegment destination = linuxUdpSocketAPI.destination(inetAddress, port);
+ final int family = inetAddress.getAddress().length == 16 ? LinuxUdpSocketAPI.AF_INET6 : LinuxUdpSocketAPI.AF_INET;
+ int fd = linuxUdpSocketAPI.socket(family);
+ try {
+ linuxUdpSocketAPI.setReceiveTimeout(fd, timeoutInMillis);
+ linuxUdpSocketAPI.connect(fd, destination);
+ linuxUdpSocketAPI.sendEmptyDatagram(fd);
+
+ MemorySegment recvBuffer = arena.allocate(RECV_BUFFER_SIZE);
+ try {
+ int bytesRead = linuxUdpSocketAPI.recv(fd, recvBuffer);
+ if (bytesRead >= 0) {
+ return PortState.OPEN;
+ }
+ } catch (RawLinuxSocketException e) {
+ int errno = e.errno;
+ if (errno == LinuxUdpSocketAPI.ECONNREFUSED) {
+ return PortState.CLOSED;
+ }
+ if (errno == LinuxUdpSocketAPI.EAGAIN) {
+ return PortState.FILTERED;
+ }
+ throw new IOException("recv() failed: errno=" + errno);
+ }
+ } finally {
+ linuxUdpSocketAPI.close(fd);
+ }
+ }
+
+ return PortState.UNKNOWN;
+ }
+}
diff --git a/src/main/java/com/it_jaros/jns/scan/engine/platform/NativeCallException.java b/src/main/java/com/it_jaros/jns/scan/engine/platform/NativeCallException.java
new file mode 100644
index 0000000..3373ad5
--- /dev/null
+++ b/src/main/java/com/it_jaros/jns/scan/engine/platform/NativeCallException.java
@@ -0,0 +1,7 @@
+package com.it_jaros.jns.scan.engine.platform;
+
+public class NativeCallException extends Exception {
+ public NativeCallException(String message, Throwable t) {
+ super(message, t);
+ }
+}
diff --git a/src/main/java/com/it_jaros/jns/scan/engine/platform/linux/LinuxUdpSocketAPI.java b/src/main/java/com/it_jaros/jns/scan/engine/platform/linux/LinuxUdpSocketAPI.java
new file mode 100644
index 0000000..fe087d7
--- /dev/null
+++ b/src/main/java/com/it_jaros/jns/scan/engine/platform/linux/LinuxUdpSocketAPI.java
@@ -0,0 +1,210 @@
+package com.it_jaros.jns.scan.engine.platform.linux;
+
+import com.it_jaros.jns.scan.engine.platform.NativeCallException;
+
+import java.io.IOException;
+import java.lang.foreign.*;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.VarHandle;
+import java.net.InetAddress;
+import java.nio.ByteOrder;
+
+public class LinuxUdpSocketAPI {
+
+ public static final int AF_INET = 2;
+ public static final int AF_INET6 = 10;
+ public static final int EAGAIN = 11;
+ public static final int ECONNREFUSED = 111;
+ public static final int EWOULDBLOCK = 11;
+ public static final int SOCK_DGRAM = 2;
+ public static final int SOL_SOCKET = 1;
+ public static final int SO_RCVTIMEO = 20;
+
+ private static final Linker LINKER = Linker.nativeLinker();
+ private static final SymbolLookup LOOKUP = LINKER.defaultLookup();
+
+ private static final Linker.Option CAPTURE_ERRNO =
+ Linker.Option.captureCallState("errno");
+
+ private static final StructLayout CALL_STATE_LAYOUT =
+ Linker.Option.captureStateLayout();
+
+ private static final VarHandle ERRNO =
+ CALL_STATE_LAYOUT.varHandle(
+ MemoryLayout.PathElement.groupElement("errno")
+ );
+
+
+ private static final MethodHandle SOCKET = LINKER.downcallHandle(
+ LOOKUP.findOrThrow("socket"),
+ FunctionDescriptor.of(
+ ValueLayout.JAVA_INT,
+ ValueLayout.JAVA_INT,
+ ValueLayout.JAVA_INT,
+ ValueLayout.JAVA_INT
+ ),
+ CAPTURE_ERRNO
+ );
+ private static final MethodHandle CONNECT = LINKER.downcallHandle(
+ LOOKUP.findOrThrow("connect"),
+ FunctionDescriptor.of(
+ ValueLayout.JAVA_INT,
+ ValueLayout.JAVA_INT,
+ ValueLayout.ADDRESS,
+ ValueLayout.JAVA_INT
+ ),
+ CAPTURE_ERRNO
+ );
+
+ private static final MethodHandle SEND = LINKER.downcallHandle(
+ LOOKUP.findOrThrow("send"),
+ FunctionDescriptor.of(
+ ValueLayout.JAVA_LONG, // ssize_t return
+ ValueLayout.JAVA_INT, // int sockfd
+ ValueLayout.ADDRESS, // const void *buf
+ ValueLayout.JAVA_LONG, // size_t len
+ ValueLayout.JAVA_INT // int flags
+ ),
+ CAPTURE_ERRNO
+ );
+
+ private static final MethodHandle RECV = LINKER.downcallHandle(
+ LOOKUP.findOrThrow("recv"),
+ FunctionDescriptor.of(
+ ValueLayout.JAVA_LONG, // ssize_t
+ ValueLayout.JAVA_INT, // sockfd
+ ValueLayout.ADDRESS, // void *buf
+ ValueLayout.JAVA_LONG, // size_t len
+ ValueLayout.JAVA_INT // flags
+ ),
+ CAPTURE_ERRNO
+ );
+
+ private static final MethodHandle SETSOCKOPT = LINKER.downcallHandle(
+ LOOKUP.findOrThrow("setsockopt"),
+ FunctionDescriptor.of(
+ ValueLayout.JAVA_INT,
+ ValueLayout.JAVA_INT,
+ ValueLayout.JAVA_INT,
+ ValueLayout.JAVA_INT,
+ ValueLayout.ADDRESS,
+ ValueLayout.JAVA_INT
+ ),
+ CAPTURE_ERRNO
+ );
+
+ private static final MethodHandle CLOSE = LINKER.downcallHandle(
+ LOOKUP.findOrThrow("close"),
+ FunctionDescriptor.of(
+ ValueLayout.JAVA_INT,
+ ValueLayout.JAVA_INT
+ ),
+ CAPTURE_ERRNO
+ );
+
+ private Arena arena;
+
+ public LinuxUdpSocketAPI(Arena arena) {
+ this.arena = arena;
+ }
+
+ <T extends Number> T wrapNativeMethodCall(MethodHandle methodHandle, Object... argv) throws IOException {
+ MemorySegment state = arena.allocate(
+ Linker.Option.captureStateLayout()
+ );
+ Object[] newArgv = new Object[argv.length + 1];
+ newArgv[0] = state;
+ System.arraycopy(argv, 0, newArgv, 1, argv.length);
+ try {
+ final T rc = callNativeMethod(methodHandle, newArgv);
+ if (rc.intValue() < 0) {
+ throw new RawLinuxSocketException(methodHandle.toString(), errno(state));
+ }
+ return rc;
+ } catch (NativeCallException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private <T> T callNativeMethod(MethodHandle methodHandle, Object... args) throws NativeCallException {
+ try {
+ return (T) methodHandle.invokeWithArguments(args);
+ } catch (Throwable t) {
+ throw new NativeCallException(methodHandle.toString(), t);
+ }
+ }
+
+ public int socket(int family) throws IOException {
+ return wrapNativeMethodCall(SOCKET, family, SOCK_DGRAM, 0);
+ }
+
+ public void connect(int fd, MemorySegment destination) throws IOException {
+ wrapNativeMethodCall(CONNECT, fd, destination, (int) destination.byteSize());
+ }
+
+ public void sendEmptyDatagram(int fd) throws IOException {
+ wrapNativeMethodCall(SEND, fd, MemorySegment.NULL, 0L, 0);
+ }
+
+ public int recv(int fd, MemorySegment buffer) throws IOException {
+ return wrapNativeMethodCall(RECV, fd, buffer, (int) buffer.byteSize(), 0);
+ }
+
+ public void setReceiveTimeout(int fd, long timeoutInMillis) throws IOException {
+ MemorySegment timeval = arena.allocate(16); // struct timeval (two longs)
+ timeval.set(ValueLayout.JAVA_LONG, 0L, timeoutInMillis / 1000L);
+ timeval.set(ValueLayout.JAVA_LONG, 8L, (timeoutInMillis % 1000L) * 1000L);
+ wrapNativeMethodCall(SETSOCKOPT, fd, SOL_SOCKET, SO_RCVTIMEO, timeval, 16);
+ }
+
+ public void close(int fd) throws IOException {
+ wrapNativeMethodCall(CLOSE, fd);
+ }
+
+ public int errno(MemorySegment callState) {
+ return (int) ERRNO.get(callState, 0L);
+ }
+
+ public MemorySegment destination(
+ InetAddress inetAddress,
+ int port
+ ) {
+ byte[] addressBytes = inetAddress.getAddress();
+
+ boolean ipv6 = addressBytes.length == 16;
+ int size = ipv6 ? 28 : 16;
+ int family = ipv6 ? AF_INET6 : AF_INET;
+
+ MemorySegment segment = arena.allocate(size);
+
+ // sa_family_t -> native byte order
+ segment.set(
+ ValueLayout.JAVA_SHORT,
+ 0,
+ (short) family
+ );
+
+ // in_port_t -> network byte order
+ segment.set(
+ ValueLayout.JAVA_SHORT.withOrder(ByteOrder.BIG_ENDIAN),
+ 2,
+ (short) port
+ );
+
+ // IPv4: address starts at offset 4
+ // IPv6: address starts at offset 8
+ long addressOffset = ipv6 ? 8 : 4;
+
+ MemorySegment.copy(
+ addressBytes,
+ 0,
+ segment,
+ ValueLayout.JAVA_BYTE,
+ addressOffset,
+ addressBytes.length
+ );
+
+ return segment;
+ }
+}
diff --git a/src/main/java/com/it_jaros/jns/scan/engine/platform/linux/RawLinuxSocketException.java b/src/main/java/com/it_jaros/jns/scan/engine/platform/linux/RawLinuxSocketException.java
new file mode 100644
index 0000000..f93ea65
--- /dev/null
+++ b/src/main/java/com/it_jaros/jns/scan/engine/platform/linux/RawLinuxSocketException.java
@@ -0,0 +1,11 @@
+package com.it_jaros.jns.scan.engine.platform.linux;
+
+import java.io.IOException;
+
+public class RawLinuxSocketException extends IOException {
+ public final int errno;
+ public RawLinuxSocketException(String message, int errno) {
+ super(message);
+ this.errno = errno;
+ }
+} \ No newline at end of file