Building a port scanner

The next version switches to using concurrent_forall/3. Since this predicate is so new (3 days old) and I run on Windows, I installed the Windows 64-bit version of the daily build.

SWI-Prolog (threaded, 64 bits, version 8.3.2-198-gd839164c7)

Note: This code also moved executing debug/1 on the command line into a Prolog directive
:- debug(concurrent). which can easily be commented out. If you peruse the SWI-Prolog source code on GitHub you will often find these lines commented out.

Also notice how much simpler the code becomes when using concurrent_forall/3.

The hardest part about writing this was trying to understand concurrent_forall(:Generate, :Test), how did Generate and Test align with my existing code. So instead of trying to understand the code from the top down I looked at the critical predicate common to all of this which is thread_create/2 and in concurrent_forall/2 is in the line maplist(thread_create(fa_worker(Q, Me, Templ, Test)), Workers) and just figured out which variables I could set and what they needed. The only one that can be set by calling concurrent_forall/3 is Test which needs to be port_scan(IP_address,Port), after that identifying what the rest of concurrent_forall needed was easy.

For this use of concurrent_forall instead of concurrent_forall(:Generate, :Test) I think of it as concurrent_forall(:Generate unique threads values, :Call thread with unique values).

:- debug(concurrent).

concurrent_scan(IP_address,Low_port,High_port,Number_of_threads) :-
    concurrent_forall(
        between(Low_port,High_port,Port),
        port_scan(IP_address,Port),
        [threads(Number_of_threads)]
    ).

port_scan(IP_address,Port) :-
    catch(
        setup_call_cleanup(
            tcp_socket(Socket),
            (
                % Open stream socket based on TCP/IP which uses IP address and port number, i.e. INET socket
                tcp_connect(Socket, IP_address:Port),
                format('Port ~w: open~n', [Port])
            ),
            tcp_close_socket(Socket)
        ),
        error(_,_),
        true
    ).

Example run

?- concurrent_scan('140.211.166.101',75,84,3).
% [Thread 5] Running test user:port_scan('140.211.166.101',77)
% [Thread 3] Running test user:port_scan('140.211.166.101',76)
% [Thread 4] Running test user:port_scan('140.211.166.101',75)
% [Thread 5] Running test user:port_scan('140.211.166.101',78)
% [Thread 4] Running test user:port_scan('140.211.166.101',79)
% [Thread 3] Running test user:port_scan('140.211.166.101',80)
Port 80: open
% [Thread 3] Running test user:port_scan('140.211.166.101',81)
% [Thread 5] Running test user:port_scan('140.211.166.101',82)
% [Thread 4] Running test user:port_scan('140.211.166.101',83)
% [Thread 3] Running test user:port_scan('140.211.166.101',84)
true.

NB The threads are being reused this time.

A more comprehensive example. (debug/1 was commented out.)

?- time(concurrent_scan('140.211.166.101',1,65536,8192)).
Port 80: open
Port 22: open
Port 443: open
% 196,643 inferences, 0.500 CPU in 174.439 seconds (0% CPU, 393286 Lips)
true.

8192 threads checking 65536 ports in ~3 minutes.


The comment Jan W. made about

and that I asked about now makes more sense. If you read the code for concurrent_forall you will notice that to pass messages back would require adding more complexity to something that is already very complex. So I take it to mean that if you want to use concurrent_forall then use it as designed, even it is breaking some rules of thumb such as have the threads know as little as possible about the outside world.