mirror of
https://github.com/pi-hole/FTL.git
synced 2025-12-20 02:19:21 +00:00
569 lines
15 KiB
C
569 lines
15 KiB
C
/* Pi-hole: A black hole for Internet advertisements
|
||
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
|
||
* Network-wide ad blocking via your own hardware.
|
||
*
|
||
* FTL Engine
|
||
* Socket connection routines
|
||
*
|
||
* This file is copyright under the latest version of the EUPL.
|
||
* Please see LICENSE file for your rights under this license. */
|
||
|
||
#include "FTL.h"
|
||
#include "api.h"
|
||
#include "shmem.h"
|
||
|
||
// The backlog argument defines the maximum length
|
||
// to which the queue of pending connections for
|
||
// telnetfd may grow. If a connection request arrives
|
||
// when the queue is full, the client may receive an
|
||
// error with an indication of ECONNREFUSED or, if
|
||
// the underlying protocol supports retransmission,
|
||
// the request may be ignored so that a later
|
||
// reattempt at connection succeeds.
|
||
#define BACKLOG 5
|
||
|
||
// File descriptors
|
||
int socketfd, telnetfd4 = 0, telnetfd6 = 0;
|
||
bool dualstack = false;
|
||
bool ipv4telnet = false, ipv6telnet = false;
|
||
bool istelnet[MAXCONNS];
|
||
|
||
static void saveport(void)
|
||
{
|
||
FILE *f;
|
||
if((f = fopen(FTLfiles.port, "w+")) == NULL)
|
||
{
|
||
logg("WARNING: Unable to write used port to file.");
|
||
logg(" Continuing anyway (API might not find the port).");
|
||
}
|
||
else
|
||
{
|
||
fprintf(f, "%i", config.port);
|
||
fclose(f);
|
||
}
|
||
}
|
||
|
||
static bool bind_to_telnet_port_IPv4(int *socketdescriptor)
|
||
{
|
||
// IPv4 socket
|
||
*socketdescriptor = socket(AF_INET, SOCK_STREAM, 0);
|
||
|
||
if(*socketdescriptor < 0)
|
||
{
|
||
logg("Error opening IPv4 telnet socket: %s (%i)", strerror(errno), errno);
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
// Set SO_REUSEADDR to allow re-binding to the port that has been used
|
||
// previously by FTL. A common pattern is that you change FTL's
|
||
// configuration file and need to restart that server to make it reload
|
||
// its configuration. Without SO_REUSEADDR, the bind() call in the restarted
|
||
// new instance will fail if there were connections open to the previous
|
||
// instance when you killed it. Those connections will hold the TCP port in
|
||
// the TIME_WAIT state for 30-120 seconds, so you fall into case 1 above.
|
||
setsockopt(*socketdescriptor, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int));
|
||
|
||
struct sockaddr_in serv_addr4;
|
||
// set all values in the buffer to zero
|
||
memset(&serv_addr4, 0, sizeof(serv_addr4));
|
||
serv_addr4.sin_family = AF_INET;
|
||
|
||
if(config.socket_listenlocal)
|
||
serv_addr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||
else
|
||
serv_addr4.sin_addr.s_addr = INADDR_ANY;
|
||
|
||
// Bind to IPv4 port
|
||
serv_addr4.sin_port = htons(config.port);
|
||
if(bind(*socketdescriptor, (struct sockaddr *) &serv_addr4, sizeof(serv_addr4)) < 0)
|
||
{
|
||
logg("Error listening on IPv4 port %i: %s (%i)", config.port, strerror(errno), errno);
|
||
return false;
|
||
}
|
||
|
||
// The listen system call allows the process to listen on the socket for connections
|
||
if(listen(*socketdescriptor, BACKLOG) == -1)
|
||
{
|
||
logg("Error listening on IPv4 socket: %s (%i)", strerror(errno), errno);
|
||
return false;
|
||
}
|
||
|
||
logg("Listening on port %i for incoming IPv4 telnet connections", config.port);
|
||
return true;
|
||
}
|
||
|
||
static bool bind_to_telnet_port_IPv6(int *socketdescriptor)
|
||
{
|
||
// IPv6 socket
|
||
*socketdescriptor = socket(AF_INET6, SOCK_STREAM, 0);
|
||
|
||
if(*socketdescriptor < 0)
|
||
{
|
||
logg("Error opening IPv6 telnet socket: %s (%i)", strerror(errno), errno);
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
// If this flag is set to true (nonzero), then the socket is re‐
|
||
// stricted to sending and receiving IPv6 packets only. In this
|
||
// case, an IPv4 and an IPv6 application can bind to a single port
|
||
// at the same time.
|
||
setsockopt(*socketdescriptor, IPPROTO_IPV6, IPV6_V6ONLY, &(int){ 1 }, sizeof(int));
|
||
|
||
// Set SO_REUSEADDR to allow re-binding to the port that has been used
|
||
// previously by FTL. A common pattern is that you change FTL's
|
||
// configuration file and need to restart that server to make it reload
|
||
// its configuration. Without SO_REUSEADDR, the bind() call in the restarted
|
||
// new instance will fail if there were connections open to the previous
|
||
// instance when you killed it. Those connections will hold the TCP port in
|
||
// the TIME_WAIT state for 30-120 seconds, so you fall into case 1 above.
|
||
setsockopt(*socketdescriptor, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int));
|
||
|
||
struct sockaddr_in6 serv_addr;
|
||
// set all values in the buffer to zero
|
||
memset(&serv_addr, 0, sizeof(serv_addr));
|
||
serv_addr.sin6_family = AF_INET6;
|
||
|
||
if(config.socket_listenlocal)
|
||
serv_addr.sin6_addr = in6addr_loopback;
|
||
else
|
||
serv_addr.sin6_addr = in6addr_any;
|
||
|
||
// The bind() system call binds a socket to an address,
|
||
// in this case the address of the current host and
|
||
// port number on which the server will run.
|
||
// convert this to network byte order using the function htons()
|
||
// which converts a port number in host byte order to a port number
|
||
// in network byte order
|
||
|
||
// Bind to IPv6 socket
|
||
serv_addr.sin6_port = htons(config.port);
|
||
if(bind(*socketdescriptor, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
|
||
{
|
||
logg("Error listening on IPv6 port %i: %s (%i)", config.port, strerror(errno), errno);
|
||
return false;
|
||
}
|
||
|
||
// The listen system call allows the process to listen on the socket for connections
|
||
if(listen(*socketdescriptor, BACKLOG) == -1)
|
||
{
|
||
logg("Error listening on IPv6 socket: %s (%i)", strerror(errno), errno);
|
||
return false;
|
||
}
|
||
|
||
logg("Listening on port %i for incoming IPv6 telnet connections", config.port);
|
||
return true;
|
||
}
|
||
|
||
static void bind_to_unix_socket(int *socketdescriptor)
|
||
{
|
||
*socketdescriptor = socket(AF_LOCAL, SOCK_STREAM, 0);
|
||
|
||
if(*socketdescriptor < 0)
|
||
{
|
||
logg("Error opening Unix socket");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
// Make sure unix socket file handle does not exist, if it exists, remove it
|
||
unlink(FTLfiles.socketfile);
|
||
|
||
struct sockaddr_un address;
|
||
address.sun_family = AF_LOCAL;
|
||
strcpy(address.sun_path, FTLfiles.socketfile);
|
||
|
||
// Bild to Unix socket handle
|
||
errno = 0;
|
||
if(bind(*socketdescriptor, (struct sockaddr *) &address, sizeof (address)) != 0)
|
||
{
|
||
logg("Error on binding on Unix socket %s: %s (%i)", FTLfiles.socketfile, strerror(errno), errno);
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
// The listen system call allows the process to listen on the Unix socket for connections
|
||
if(listen(*socketdescriptor, BACKLOG) == -1)
|
||
{
|
||
logg("Error listening on Unix socket: %s (%i)", strerror(errno), errno);
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
logg("Listening on Unix socket");
|
||
}
|
||
|
||
// Called from main() at graceful shutdown
|
||
static void removeport(void)
|
||
{
|
||
FILE *f;
|
||
if((f = fopen(FTLfiles.port, "w+")) == NULL)
|
||
{
|
||
logg("WARNING: Unable to empty port file");
|
||
return;
|
||
}
|
||
fclose(f);
|
||
}
|
||
|
||
void seom(const int sock)
|
||
{
|
||
if(istelnet[sock])
|
||
ssend(sock, "---EOM---\n\n");
|
||
else
|
||
pack_eom(sock);
|
||
}
|
||
|
||
void __attribute__ ((format (gnu_printf, 2, 3))) ssend(const int sock, const char *format, ...)
|
||
{
|
||
char *buffer;
|
||
va_list args;
|
||
va_start(args, format);
|
||
int ret = vasprintf(&buffer, format, args);
|
||
va_end(args);
|
||
if(ret > 0)
|
||
{
|
||
if(!write(sock, buffer, strlen(buffer)))
|
||
logg("WARNING: Socket write returned error %s (%i)", strerror(errno), errno);
|
||
free(buffer);
|
||
}
|
||
}
|
||
|
||
void swrite(const int sock, const void *value, size_t size) {
|
||
if(write(sock, value, size) == -1)
|
||
logg("WARNING: Socket write returned error code %i", errno);
|
||
}
|
||
|
||
static inline int checkClientLimit(const int socket) {
|
||
if(socket < MAXCONNS)
|
||
{
|
||
return socket;
|
||
}
|
||
else
|
||
{
|
||
logg("Client denied (at max capacity of %i): %i", MAXCONNS, socket);
|
||
|
||
close(socket);
|
||
return -1;
|
||
}
|
||
}
|
||
|
||
static int listener(const int sockfd, const char type)
|
||
{
|
||
struct sockaddr_un un_addr;
|
||
struct sockaddr_in in4_addr;
|
||
struct sockaddr_in6 in6_addr;
|
||
socklen_t socklen = 0;
|
||
int socket;
|
||
|
||
switch(type)
|
||
{
|
||
case 0: // Unix socket
|
||
memset(&un_addr, 0, sizeof(un_addr));
|
||
socklen = sizeof(un_addr);
|
||
return accept(sockfd, (struct sockaddr *) &un_addr, &socklen);
|
||
|
||
case 4: // Internet socket (IPv4)
|
||
memset(&in4_addr, 0, sizeof(in4_addr));
|
||
socklen = sizeof(un_addr);
|
||
socket = accept(sockfd, (struct sockaddr *) &in4_addr, &socklen);
|
||
return checkClientLimit(socket);
|
||
|
||
case 6: // Internet socket (IPv6)
|
||
memset(&in6_addr, 0, sizeof(in6_addr));
|
||
socklen = sizeof(un_addr);
|
||
socket = accept(sockfd, (struct sockaddr *) &in6_addr, &socklen);
|
||
return checkClientLimit(socket);
|
||
|
||
default: // Should not happen
|
||
logg("Cannot listen on type %i connection, code error!", type);
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
}
|
||
|
||
void close_telnet_socket(void)
|
||
{
|
||
removeport();
|
||
// Using global variable here
|
||
if(telnetfd4)
|
||
close(telnetfd4);
|
||
if(telnetfd6)
|
||
close(telnetfd6);
|
||
}
|
||
|
||
void close_unix_socket(void)
|
||
{
|
||
// The process has to take care of unlinking the socket file description on exit
|
||
unlink(FTLfiles.socketfile);
|
||
// Using global variable here
|
||
close(socketfd);
|
||
}
|
||
|
||
static void *telnet_connection_handler_thread(void *socket_desc)
|
||
{
|
||
//Get the socket descriptor
|
||
int sock = *(int*)socket_desc;
|
||
// Set connection type to telnet
|
||
istelnet[sock] = true;
|
||
|
||
// Define buffer for client's message
|
||
char client_message[SOCKETBUFFERLEN] = "";
|
||
|
||
// Set thread name
|
||
char threadname[16];
|
||
sprintf(threadname,"telnet-%i",sock);
|
||
prctl(PR_SET_NAME,threadname,0,0,0);
|
||
//Receive from client
|
||
ssize_t n;
|
||
while((n = recv(sock,client_message,SOCKETBUFFERLEN-1, 0)))
|
||
{
|
||
if (n > 0)
|
||
{
|
||
char *message = strdup(client_message);
|
||
if(message == NULL) break;
|
||
|
||
// Clear client message receive buffer
|
||
memset(client_message, 0, sizeof client_message);
|
||
|
||
// Process received message
|
||
process_request(message, &sock);
|
||
free(message);
|
||
|
||
if(sock == 0)
|
||
{
|
||
// Client disconnected by sending EOT or ">quit"
|
||
break;
|
||
}
|
||
}
|
||
else if(n == -1)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
//Free the socket pointer
|
||
if(sock != 0)
|
||
close(sock);
|
||
free(socket_desc);
|
||
|
||
return false;
|
||
}
|
||
|
||
|
||
static void *socket_connection_handler_thread(void *socket_desc)
|
||
{
|
||
//Get the socket descriptor
|
||
int sock = *(int*)socket_desc;
|
||
// Set connection type to not telnet
|
||
istelnet[sock] = false;
|
||
|
||
// Define buffer for client's message
|
||
char client_message[SOCKETBUFFERLEN] = "";
|
||
|
||
// Set thread name
|
||
char threadname[16];
|
||
sprintf(threadname,"socket-%i",sock);
|
||
prctl(PR_SET_NAME,threadname,0,0,0);
|
||
//Receive from client
|
||
ssize_t n;
|
||
while((n = recv(sock,client_message,SOCKETBUFFERLEN-1, 0)))
|
||
{
|
||
if (n > 0)
|
||
{
|
||
char *message = strdup(client_message);
|
||
if(message == NULL) break;
|
||
|
||
// Clear client message receive buffer
|
||
memset(client_message, 0, sizeof client_message);
|
||
|
||
// Process received message
|
||
process_request(message, &sock);
|
||
free(message);
|
||
|
||
if(sock == 0)
|
||
{
|
||
// Socket connection interrupted by sending EOT or ">quit"
|
||
break;
|
||
}
|
||
}
|
||
else if(n == -1)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
//Free the socket pointer
|
||
if(sock != 0)
|
||
close(sock);
|
||
free(socket_desc);
|
||
|
||
return false;
|
||
}
|
||
|
||
void bind_sockets(void)
|
||
{
|
||
// Initialize IPv4 telnet socket
|
||
if(bind_to_telnet_port_IPv4(&telnetfd4))
|
||
ipv4telnet = true;
|
||
|
||
// Initialize IPv6 telnet socket
|
||
// only if IPv6 interfaces are available
|
||
if(ipv6_available())
|
||
if(bind_to_telnet_port_IPv6(&telnetfd6))
|
||
ipv6telnet = true;
|
||
|
||
saveport();
|
||
|
||
// Initialize Unix socket
|
||
bind_to_unix_socket(&socketfd);
|
||
}
|
||
|
||
void *telnet_listening_thread_IPv4(void *args)
|
||
{
|
||
// We will use the attributes object later to start all threads in detached mode
|
||
pthread_attr_t attr;
|
||
// Initialize thread attributes object with default attribute values
|
||
pthread_attr_init(&attr);
|
||
// When a detached thread terminates, its resources are automatically released back to
|
||
// the system without the need for another thread to join with the terminated thread
|
||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||
|
||
// Set thread name
|
||
prctl(PR_SET_NAME,"telnet-IPv4",0,0,0);
|
||
|
||
// Listen as long as FTL is not killed
|
||
while(!killed)
|
||
{
|
||
// Look for new clients that want to connect
|
||
const int csck = listener(telnetfd4, 4);
|
||
if(csck == -1)
|
||
{
|
||
logg("IPv4 telnet error: %s (%i)", strerror(errno), errno);
|
||
continue;
|
||
}
|
||
|
||
// Allocate memory used to transport client socket ID to client listening thread
|
||
int *newsock;
|
||
newsock = calloc(1,sizeof(int));
|
||
if(newsock == NULL) break;
|
||
*newsock = csck;
|
||
|
||
pthread_t telnet_connection_thread;
|
||
// Create a new thread
|
||
if(pthread_create( &telnet_connection_thread, &attr, telnet_connection_handler_thread, (void*) newsock ) != 0)
|
||
{
|
||
// Log the error code description
|
||
logg("WARNING: Unable to open telnet processing thread, error: %s", strerror(errno));
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void *telnet_listening_thread_IPv6(void *args)
|
||
{
|
||
// We will use the attributes object later to start all threads in detached mode
|
||
pthread_attr_t attr;
|
||
// Initialize thread attributes object with default attribute values
|
||
pthread_attr_init(&attr);
|
||
// When a detached thread terminates, its resources are automatically released back to
|
||
// the system without the need for another thread to join with the terminated thread
|
||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||
|
||
// Set thread name
|
||
prctl(PR_SET_NAME,"telnet-IPv6",0,0,0);
|
||
|
||
// Listen as long as FTL is not killed
|
||
while(!killed)
|
||
{
|
||
// Look for new clients that want to connect
|
||
const int csck = listener(telnetfd6, 6);
|
||
if(csck == -1)
|
||
{
|
||
logg("IPv6 telnet error: %s (%i)", strerror(errno), errno);
|
||
continue;
|
||
}
|
||
|
||
// Allocate memory used to transport client socket ID to client listening thread
|
||
int *newsock;
|
||
newsock = calloc(1,sizeof(int));
|
||
if(newsock == NULL) break;
|
||
*newsock = csck;
|
||
|
||
pthread_t telnet_connection_thread;
|
||
// Create a new thread
|
||
if(pthread_create( &telnet_connection_thread, &attr, telnet_connection_handler_thread, (void*) newsock ) != 0)
|
||
{
|
||
// Log the error code description
|
||
logg("WARNING: Unable to open telnet processing thread, error: %s", strerror(errno));
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void *socket_listening_thread(void *args)
|
||
{
|
||
// We will use the attributes object later to start all threads in detached mode
|
||
pthread_attr_t attr;
|
||
// Initialize thread attributes object with default attribute values
|
||
pthread_attr_init(&attr);
|
||
// When a detached thread terminates, its resources are automatically released back to
|
||
// the system without the need for another thread to join with the terminated thread
|
||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||
|
||
// Set thread name
|
||
prctl(PR_SET_NAME,"socket listener",0,0,0);
|
||
|
||
// Listen as long as FTL is not killed
|
||
while(!killed)
|
||
{
|
||
// Look for new clients that want to connect
|
||
const int csck = listener(socketfd, 0);
|
||
if(csck < 0) continue;
|
||
|
||
// Allocate memory used to transport client socket ID to client listening thread
|
||
int *newsock;
|
||
newsock = calloc(1,sizeof(int));
|
||
if(newsock == NULL) break;
|
||
*newsock = csck;
|
||
|
||
pthread_t socket_connection_thread;
|
||
// Create a new thread
|
||
if(pthread_create( &socket_connection_thread, &attr, socket_connection_handler_thread, (void*) newsock ) != 0)
|
||
{
|
||
// Log the error code description
|
||
logg("WARNING: Unable to open socket processing thread, error: %s", strerror(errno));
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool ipv6_available(void)
|
||
{
|
||
struct ifaddrs *allInterfaces;
|
||
int iface[2] = { 0 };
|
||
|
||
// Get all interfaces
|
||
if (getifaddrs(&allInterfaces) == 0)
|
||
{
|
||
struct ifaddrs *interface;
|
||
// Loop over interfaces
|
||
for (interface = allInterfaces; interface != NULL; interface = interface->ifa_next)
|
||
{
|
||
const unsigned int flags = interface->ifa_flags;
|
||
const struct sockaddr *addr = interface->ifa_addr;
|
||
|
||
// Check only for up and running IPv4, IPv6 interfaces
|
||
if ((flags & (IFF_UP|IFF_RUNNING)) && addr != NULL)
|
||
{
|
||
iface[addr->sa_family == AF_INET6 ? 1 : 0]++;
|
||
|
||
if(config.debug & DEBUG_NETWORKING)
|
||
logg("Interface %s is %s", interface->ifa_name, addr->sa_family == AF_INET6 ? "IPv6" : "IPv4");
|
||
}
|
||
}
|
||
freeifaddrs(allInterfaces);
|
||
}
|
||
|
||
if(config.debug & DEBUG_NETWORKING)
|
||
{
|
||
logg("Found %i IPv4 and %i IPv6 capable interfaces", iface[0], iface[1]);
|
||
}
|
||
|
||
return (iface[1] > 0);
|
||
}
|