TCP Port Scanner Example using Spring Shell
This post is about using Spring Shell to make a simple application for scanning open TCP ports.
Technologies used:
- Spring Boot 2.0.5.RELEASE
- Spring Shell 2.0.1.RELEASE
Quick Overview:
- Final Project Structure
- Creating a new base Spring Boot project
- Needs for parallelism
- How check whether a port is open?
- Integrating with Spring Shell
- How it works
Final Project Structure
Creating a new base Spring Boot project
We will start with a new project generated by Spring Initializr. We need just only one Spring dependency, i.e. Spring Shell.
All required dependencies are shown here:
<dependencies>
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
</dependencies>
Needs for parallelism
Scanning open ports can take a lot of time, especially when you are scanning thousands of ports. It is clear we need to parallel this task. The level of the parallelism can be defined with a number of possible threads inside our thread pool (just to be sure, we will use async support from Spring):
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Value("${threads.count:20}")
private int threadsCount;
@Override
public Executor getAsyncExecutor() {
return Executors.newFixedThreadPool(threadsCount);
}
}
How check whether a port is open?
Each of tries to check whether a port is open will contain the same step, i.e. a connect using java.net.Socket
to a specific IP address and a specific port. When a connection timeout occurs a port is not open. Take notice that our method is annotated with @Async
and return type is Future
.
It means that everyone who calls this method will not be blocked until the end of a scanning process. In other words, this method starts running in another thread than the caller’s thread.
@Async
public Future<ScanResult> checkPort(String ip, int port) {
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(ip, port), timeout);
socket.close();
return new AsyncResult<>(new ScanResult(port, true));
} catch (IOException ex) {
return new AsyncResult<>(new ScanResult(port, false));
}
}
Integrating with Spring Shell
Our goal is to create an application which will be able to scan open ports on a specific IP address. So it would be fine to create a command line application and Spring Shell will help us to do that.
At first, we need to create a new command so we will create a new class. Each of a method it will be a standalone command callable from a command line. The Spring Shell just only needs to know about this class and so-called method commands.
All of this can be configured using annotations: @ShellComponent
on a class level and @ShellMethod
on a method level. Command parameters are the same as method parameters with the possibility to customize it using @ShellOption
.
Here is our implementation. First of all, we create a task for each of port to scan (calling method addToScan
) and then we print a result to an output.
There is some magic to be able to scan either a single port or range of ports:
@ShellComponent
public class ScannerCommand {
public static final String PORT_SEPARATOR = "-";
private final ScannerService scannerService;
@Autowired
public ScannerCommand(ScannerService scannerService) {
this.scannerService = scannerService;
}
@ShellMethod(value = "Scan open ports for a specific IP address")
public String scan(
@ShellOption(help = "IP address") String ip,
@ShellOption(help = "Port or port range, e.g. 1-1024") String port,
@ShellOption(help = "Weather only open ports should be displayed") boolean displayOnlyOpen
) throws ExecutionException, InterruptedException {
//Add all required ports into port scanner
List<Future<ScannerService.ScanResult>> futureList;
if (port.contains(PORT_SEPARATOR)) {
String[] rangeLimits = port.split(PORT_SEPARATOR);
futureList = addToScan(ip, range(Integer.parseInt(rangeLimits[0]), Integer.parseInt(rangeLimits[1])));
} else {
futureList = addToScan(ip, Integer.parseInt(port));
}
//Read and write results
for (final Future<ScannerService.ScanResult> scanResultFuture : futureList) {
ScannerService.ScanResult scanResult = scanResultFuture.get();
if (displayOnlyOpen) {
if (scanResult.isOpen()) {
System.out.println(scanResult);
}
} else {
System.out.println(scanResult);
}
}
return "DONE";
}
private List<Future<ScannerService.ScanResult>> addToScan(String ip, int... ports) {
List<Future<ScannerService.ScanResult>> result = new ArrayList<>();
for (int port : ports) {
result.add(scannerService.checkPort(ip, port));
}
return result;
}
}
How it works
When you successfully run our application, you will see a command line prompt:
shell:>
You can use some predefined command such a help with our scan command:
shell:> help scan
NAME
scan - Scan open ports for a specific IP address
SYNOPSYS
scan [--ip] string [--port] string [--display-only-open]
OPTIONS
--ip string
IP address
[Mandatory]
--port string
Port or port range, e.g. 1-1024
[Mandatory]
--display-only-open Weather only open ports should be displayed
[Optional, default = false]
So let’s try our command with the option –display-only-open to display open ports only:
shell:>scan --ip 10.15.13.52 --port 1-1024 --display-only-open
port 22 - open
port 25 - open
DONE
You can find all source codes on my GitHub profile.
Some useful links about this topic: