mirror of
https://tildegit.org/solene/iblock.git
synced 2024-11-23 10:39:53 +01:00
Merge pull request 'icanserve' (#5) from prx/iblock:icanserve into main
Reviewed-on: https://tildegit.org/solene/iblock/pulls/5
This commit is contained in:
commit
b98e616f34
2
Makefile
2
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
|
||||
|
44
README.md
44
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 <blocked> persist
|
||||
table <iblocked> persist
|
||||
|
||||
block in quick from <blocked> 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 <iblocked> 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
|
||||
|
30
iblock.8
Normal file
30
iblock.8
Normal file
@ -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
|
11
iblock.rc
Executable file
11
iblock.rc
Executable file
@ -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
|
286
main.c
286
main.c
@ -1,90 +1,262 @@
|
||||
#include <sys/types.h>
|
||||
#include <sys/event.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <net/if.h>
|
||||
#include <net/pfvar.h>
|
||||
|
||||
#include <err.h>
|
||||
#include <netdb.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <syslog.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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 <table>) (-p <port>)\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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user