diff --git a/Makefile b/Makefile index 5756b59..8d84670 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,8 @@ clean: install: iblock install -o root -g wheel iblock ${PREFIX}/sbin/ + install -o root -g wheel iblock.rc /etc/rc.d/iblock + install -o root -g wheel iblock.8 ${PREFIX}/man/man8/ test: clean iblock @printf "hello\n" | nc -4 localhost 666 diff --git a/README.md b/README.md index 8eb959a..e160584 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # iblock -iblock is an inetd program adding the client IP to a Packet Filter table. +iblock is a program adding the client IP to a Packet Filter table. It is meant to be used to block scanner connecting on unused ports. @@ -22,26 +22,6 @@ Add in `/etc/doas.conf`: permit nopass _iblock cmd /sbin/pfctl ``` -## Configure inetd - -Start inetd service with this in `/etc/inetd.conf`: - -``` -666 stream tcp nowait _iblock /usr/local/sbin/iblock iblock -666 stream tcp6 nowait _iblock /usr/local/sbin/iblock iblock -``` - -You can change the PF table by adding it as a parameter like this: - -In this example, the parameter `blocklist` will add IPs to the `blocklist` PF table. - -``` -666 stream tcp nowait _iblock /usr/local/sbin/iblock iblock blocklist -666 stream tcp6 nowait _iblock /usr/local/sbin/iblock iblock blocklist -``` - -Default is "iblocked" table. - ## Configure packet filter Use this in `/etc/pf.conf`, choose which ports will trigger the ban from the variable: @@ -50,22 +30,24 @@ Use this in `/etc/pf.conf`, choose which ports will trigger the ban from the var # services triggering a block blocking_tcp="{ 21 23 53 111 135 137:139 445 1433 25565 5432 3389 3306 27019 }" -table persist +table persist -block in quick from label iblock -pass in quick on egress inet proto tcp to port $blocking_tcp rdr-to 127.0.0.1 port 666 -pass in quick on egress inet6 proto tcp to port $blocking_tcp rdr-to ::1 port 666 +block in quick from label iblock +# iblock listens on port 2507 +pass in quick on egress inet proto tcp to port $blocking_tcp rdr-to 127.0.0.1 port 2507 +pass in quick on egress inet6 proto tcp to port $blocking_tcp rdr-to ::1 port 2507 ``` Don't forget to reload the rules with `pfctl -f /etc/pf.conf`. +Use another table or port name by passing appropriate flags to iblock: + +``` +rcctl set iblock flags -t another_table_name -p 5373 +``` + # Get some statistics -Done! You can see IP banned using `pfctl -t blocked -T show` and iBlock will send blocked addresses to syslog. +Done! You can see IP banned using `pfctl -t iblocked -T show` and iblock will send blocked addresses to syslog. In the example I added a label to the block rule, you can use `pfctl -s labels` to view statistics from this rule, [see documentation for column meaning](https://man.openbsd.org/pfctl#s~8). - - -# TODO - -- A proper man page diff --git a/iblock.8 b/iblock.8 new file mode 100644 index 0000000..65c2617 --- /dev/null +++ b/iblock.8 @@ -0,0 +1,30 @@ +.Dd $Mdocdate: September 03 2023 $ +.Dt iblock 8 +.Os +.Sh NAME +.Nm iblock +.Nd add unwanted IP to pf table +.Sh SYNOPSIS +.Nm iblock +.Op Fl t Ar table +.Op Fl p Ar port +.Sh DESCRIPTION +.Nm +is a program adding the client IP to a Packet Filter table. +.Pp +It is meant to be used to block scanner connecting on unused ports. +Upon connection, the IP is added to a PF table and all established connections with this IP are killed. You need to use a PF bloking rule using the table. + +.Sh OPTIONS +.Bl -tag -width Ds +.It Op Fl t Ar table +Set the pf +.Ar table +to add the detected IP. +.It Op Fl p Ar port +Set the listening +.Ar port . +.El +.Sh DEPLOYMENT + +TODO diff --git a/iblock.rc b/iblock.rc new file mode 100755 index 0000000..d342bf1 --- /dev/null +++ b/iblock.rc @@ -0,0 +1,11 @@ +#!/bin/ksh + +daemon="/usr/local/sbin/iblock" +daemon_user="_iblock" + +. /etc/rc.d/rc.subr + +rc_reload=NO +rc_bg=YES + +rc_cmd $1 diff --git a/main.c b/main.c index cafcebc..2af3da4 100644 --- a/main.c +++ b/main.c @@ -1,90 +1,262 @@ +#include +#include #include #include #include +#include + +#include +#include #include #include #include #include +#include #include #include #define DEFAULT_TABLE "iblocked" +#define DEFAULT_PORT "2507" +#define MAXSOCK 2 /* ipv4 + ipv6 */ +#define BACKLOG 10 -static void __dead +static void ban(const char *, const char *); +static void *get_in_addr(struct sockaddr *); +static void runcmd(const char**); +static int setup_server(const char*, int *); +static void usage(void); +static void watch_event(const int, const int *, const char *); + + +static void +ban(const char *ip, const char *table) +{ + + const char *bancmd[] = { "/usr/bin/doas", "-n", + "/sbin/pfctl", "-t", table, + "-T", "add", ip, + NULL }; + const char *killstatecmd[] = { "/usr/bin/doas", "-n", + "/sbin/pfctl", + "-k", ip, + NULL }; + + syslog(LOG_DAEMON, "block and kill states for %s", ip); + runcmd(bancmd); + runcmd(killstatecmd); +} + +/* return printable ip from sockaddr */ +static void +*get_in_addr(struct sockaddr *sa) +{ + if (sa->sa_family == AF_INET) + return &(((struct sockaddr_in*)sa)->sin_addr); + + return &(((struct sockaddr_in6*)sa)->sin6_addr); +} + +/* run cmd in execv() after fork() */ +static void +runcmd(const char **cmd_arg_list) +{ + pid_t pid = fork(); + if (pid == -1) { + syslog(LOG_DAEMON, "fork error"); + err(1,"fork"); + } else if (pid == 0) { /* child */ + execv(cmd_arg_list[0], (char **)cmd_arg_list); + /* if this is reached, then exec failed */ + syslog(LOG_DAEMON, "execv error"); + err(1,"execv"); + } else { /* parent */ + waitpid(pid, NULL, 0); + } +} + +static int +setup_server(const char *port, int s[]) +{ + int nsock = 0; + char server_ip[INET6_ADDRSTRLEN] = {'\0'}; + const char *err_cause = NULL; + struct addrinfo hints, *servinfo, *res; + + /* initialize structures */ + memset(&hints, 0, sizeof(hints)); + + /* set hints for socket */ + hints.ai_family = AF_UNSPEC; /* ip4 or ip6 */ + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + /* get ips for localhost */ + int retval = getaddrinfo("localhost", port, &hints, &servinfo); + if (retval != 0) { + syslog(LOG_DAEMON, "getaddrinfo failed"); + err(1, "getaddrinfo :%s", gai_strerror(retval)); + } + + /* create sockets and bind for each local ip, store them in s[] */ + for (res = servinfo; res && nsock < MAXSOCK; res = res->ai_next) { + + s[nsock] = socket(res->ai_family, + res->ai_socktype, + res->ai_protocol); + if (s[nsock] == -1) { + err_cause = "socket"; + continue; + } + /* make sure PORT can be reused by second IP */ + int yes = 1; + if (setsockopt(s[nsock], SOL_SOCKET, SO_REUSEPORT, &yes, + sizeof(int)) == -1) + err(1, "setsockopt"); + + if (bind(s[nsock], res->ai_addr, res->ai_addrlen) == -1) { + close(s[nsock]); + err_cause = "bind()"; + continue; + } + + if (listen(s[nsock], BACKLOG) == -1) + err_cause = "listen"; + + /* log the obtained ip */ + inet_ntop(res->ai_family, + get_in_addr((struct sockaddr *)res->ai_addr), + server_ip, sizeof(server_ip)); + syslog(LOG_DAEMON, "listening on %s port %s, muahaha :>", + server_ip, + port); + + nsock++; + } + + /* clean up no longer used servinfo */ + freeaddrinfo(servinfo); + + if (nsock == 0) + err(1, "Error when calling %s", err_cause); + + return nsock; +} + +static void usage(void) { - fprintf(stderr, "usage: %s [table]\n", getprogname()); + fprintf(stderr, "usage: %s (-t ) (-p )\n", + getprogname()); exit(1); } +static void +watch_event(const int nsock, const int s[], const char *table) +{ + int kq = 0; + int new_fd = 0; + char ip[INET6_ADDRSTRLEN] = {'\0'}; + struct kevent ev[MAXSOCK] = {0}; + socklen_t sin_size = 0; + struct sockaddr_storage client_addr; + + + /* initialize structures */ + memset(&client_addr, 0, sizeof(client_addr)); + + /* configure events */ + kq = kqueue(); + + /* add event for each IP */ + for (int i = 0; i < nsock; i++) + EV_SET(&(ev[i]), s[i], + EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0); + + /* register event */ + if (kevent(kq, ev, MAXSOCK, NULL, 0, NULL) == -1) + err(1, "kevent register"); + + /* infinite loop to wait for connections */ + for (;;) { + int nevents = kevent(kq, NULL, 0, ev, MAXSOCK, NULL); + if (nevents == -1) + err(1, "kevent get event"); + + /* loop for events */ + for (int i = 0; i < nevents; i++) { + + if (ev[i].filter & EVFILT_READ) { + + /* get client ip */ + sin_size = sizeof(client_addr); + new_fd = accept(ev[i].ident, + (struct sockaddr*)&client_addr, + &sin_size); + if (new_fd == -1) + continue; + inet_ntop(client_addr.ss_family, + get_in_addr((struct sockaddr *)&client_addr), + ip, sizeof(ip)); + + close(new_fd); /* no longer required */ + + ban(ip, table); /* ban this ip */ + } + if (ev[i].filter & EVFILT_SIGNAL) { + break; + } + } /* events loop */ + } /* infinite loop */ + + /* probably never reached, but close properly */ + close(kq); +} + int main(int argc, char *argv[]) { - struct sockaddr_storage sock = {0}; - socklen_t slen = sizeof(sock); - char ip[INET6_ADDRSTRLEN] = {'\0'}; /* INET6_ADDRSTRLEN > INET_ADDRSTRLEN */ - const char *table = DEFAULT_TABLE; - int ch, status = 0; - pid_t id; + char table[PF_TABLE_NAME_SIZE] = DEFAULT_TABLE; + char port[6] = DEFAULT_PORT; + int nsock = 0; + int option = 0; + int s[MAXSOCK] = {0}; - if (unveil("/usr/bin/doas", "rx") != 0) - err(1, "unveil"); - if (pledge("exec inet proc stdio", NULL) != 0) - err(1, "pledge"); - while ((ch = getopt(argc, argv, "")) != -1) { - switch (ch) { + while ((option = getopt(argc, argv, "t:p:")) != -1) { + switch (option) { + case 'p': + if (strlcpy(port, optarg, sizeof(port)) >= + sizeof(port)) + err(1, "invalid port"); + break; + case 't': + if (strlcpy(table, optarg, sizeof(table)) >= + sizeof(table)) + err(1, "table name too long"); + break; default: usage(); + break; } } - argc -= optind; - argv += optind; - if (argc > 1) - usage(); - if (argc == 1) - table = *argv; + /* safety first */ + if (unveil("/usr/bin/doas", "rx") != 0) + err(1, "unveil"); + /* necessary to resolve localhost with getaddrinfo() */ + if (unveil("/etc/hosts", "r") != 0) + err(1, "unveil"); + if (pledge("stdio inet exec proc rpath", NULL) != 0) + err(1, "pledge"); - /* get socket structure */ - if (getpeername(STDIN_FILENO, (struct sockaddr *)&sock, &slen)) - err(1, "getpeername"); + nsock = setup_server(port, s); + watch_event(nsock, s, table); - /* get ip */ - status = getnameinfo((struct sockaddr *)&sock, slen, ip, sizeof(ip), - NULL, 0, NI_NUMERICHOST); - - if (status != 0) { - syslog(LOG_DAEMON, "getnameinfo error: %s", - gai_strerror(status)); - exit(1); - } - - switch (sock.ss_family) { - case AF_INET: /* FALLTHROUGH */ - case AF_INET6: - id = fork(); - - if (id == -1) { - syslog(LOG_DAEMON, "fork error"); - exit(1); - } else if (id == 0) { - // child process - syslog(LOG_DAEMON, "blocking %s", ip); - execl("/usr/bin/doas", "doas", "/sbin/pfctl", - "-t", table, "-T", "add", ip, NULL); - } else { - // parent process - wait(NULL); - syslog(LOG_DAEMON, "kill states for %s", ip); - execl("/usr/bin/doas", "doas", "/sbin/pfctl", - "-k", ip, NULL); - } - break; - default: - exit(2); - } + /* probably never reached, but close properly */ + for (int i = 0; i < nsock; i++) + close(s[i]); + return 0; }