Newer
Older
/*
* filter.c: URL filter daemon
*
* Copyright (C) 2021-2024 iopsys Software Solutions AB. All rights reserved.
*
* Author: Jomily Joseph <jomily.joseph@iopsys.eu>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/netfilter.h>
#include <syslog.h>
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <libnetfilter_queue/linux_nfnetlink_queue.h>
#include <libubox/uloop.h>
#include <libbbfdm-ubus/bbfdm-ubus.h>
#include "filter_utils.h"
#include "utils.h"
#include "dns_cache.h"
extern DM_MAP_OBJ tDynamicObj[];
static struct bbfdm_context bbfdm_ctx = {0};
static struct ubus_event_handler config_reload_ev = {0};
struct nfq_handle *nfq_h;
struct nfq_q_handle *queue_h;
static struct profile *get_profile_for_mac_address(char *mac_addr)
{
struct profile *p = NULL;
// Iterate over the global profile list
list_for_each_entry(p, &profiles, list) {
// check if host is present in this profile
if (p->hosts.cmph_hash && p->hosts.data) {
// check stringstore from memory
if (stringstore_check_string_from_memory(&p->hosts, mac_addr)) {
return p;
}
}
return NULL;
}
// Checks if the packet needs to be accepted or not based on mac address
// and url.
// @param payload packet payload
// @return PKT_ACCEPT or PKT_DROP
static int handle_packet_filtering(struct nfq_data *payload)
{
struct nfqnl_msg_packet_hw *hw_hdr;
unsigned char *data = NULL;
char payload_pkt_arr[INET6_ADDRSTRLEN] = {0};
char *payload_pkt = payload_pkt_arr;
// Get the source mac address.
hw_hdr = nfq_get_packet_hw(payload);
if (!hw_hdr) {
return PKT_ACCEPT;
}
bool is_mac_zero = true;
for (int i = 0; i < 6; i++) {
if (hw_hdr->hw_addr[i] != 0) {
is_mac_zero = false;
break;
}
}
// a lot of packets are received with mac address all zeroes
// it is said that this happens when a packet is retrieved at a
// layer where mac addresses are not available, like above link layer
// however, it has been observed that these packets are safe to discard
if (is_mac_zero) {
return PKT_ACCEPT;
}
char mac_addr[MAC_LENGTH] = {0};
// Only format if the MAC address is not all zeroes
// get mac_addr in lower case (%02x for lower case %02X for upper case)
snprintf(mac_addr, sizeof(mac_addr), "%02x:%02x:%02x:%02x:%02x:%02x",
hw_hdr->hw_addr[0], hw_hdr->hw_addr[1], hw_hdr->hw_addr[2],
hw_hdr->hw_addr[3], hw_hdr->hw_addr[4], hw_hdr->hw_addr[5]);
if (mac_addr[0] == '\0') {
ret = nfq_get_payload(payload, &data);
if (ret <= 0) {
// Get the payload offset.
int payload_offset;
payload_offset = get_ip_packet_payload(data, &proto, data_len, &is_ipv6);
// Just accept the packet and return since the packet either does not match
ret = get_payload_pkt(proto, data, payload_offset,
&payload_pkt, &is_https_packet, is_ipv6);
if (ret != URL_SUCCESS)
struct profile *pkt_profile = get_profile_for_mac_address(mac_addr);
if (pkt_profile == NULL)
return PKT_ACCEPT;
// Check if the URL has been blacklisted in this profile.
ret = profile_based_url_filtering(pkt_profile, payload_pkt, mac_addr, is_https_packet, is_ipv6);
return ret;
}
// Callback function for handling packets.
// @param queue_h queue 0 handle
// @param nfmsg nfq message received
// @param nfa message data
// @param data store data
// @return 0 on success and non zero otherwise
static int handle_packets(struct nfq_q_handle *q_h, struct nfgenmsg *nfmsg __attribute__((unused)),
struct nfq_data *nfa, void *data __attribute__((unused)))
{
struct nfqnl_msg_packet_hdr *pkt_hdr;
int pkt_decision;
uint32_t pkt_id = 0;
// Fetch the packet header.
pkt_hdr = nfq_get_msg_packet_hdr(nfa);
if (pkt_hdr)
pkt_id = ntohl(pkt_hdr->packet_id);
// Check if we should block this packet on the basis of URL and source mac address.
pkt_decision = handle_packet_filtering(nfa);
if (pkt_decision == PKT_ACCEPT)
return nfq_set_verdict(q_h, pkt_id, NF_ACCEPT, 0, NULL);
return nfq_set_verdict(q_h, pkt_id, NF_DROP, 0, NULL);
}
// Function to unbind and bind to nf_queue handler.
// @return nothing
static void update_nf_queue_handler(void)
{
int nfq_bind_v4 = 1, nfq_bind_v6 = 1;
// Unbinding existing nf_queue handler.
if (nfq_unbind_pf(nfq_h, AF_INET) < 0) {
log_error("Error during nfq_unbind_pf() for IPv4");
if (nfq_unbind_pf(nfq_h, AF_INET6) < 0) {
log_error("Error during nfq_unbind_pf() for IPv6");
if (!(nfq_bind_v4 || nfq_bind_v6)) {
log_error("Error during nfq_unbind_pf()");
exit(1);
}
// Binding nfnetlink_queue as nf_queue handler.
nfq_bind_v4 = nfq_bind_v6 = 1;
if (nfq_bind_pf(nfq_h, AF_INET < 0)) {
log_error("Error during nfq_bind_pf() for IPv4");
if (nfq_bind_pf(nfq_h, AF_INET6) < 0) {
log_error("Error during nfq_bind_pf() for IPv6");
if (!(nfq_bind_v4 || nfq_bind_v6)) {
log_error("Error during nfq_bind_pf()");
exit(1);
}
}
// Open a netlink connection and returns file descriptor
// @return fd of the netlink socket
static int open_netlink_connection(void)
{
// Opening netlink socket.
nfq_h = nfq_open();
if (!nfq_h) {
log_error("Error during nfq_open()");
exit(1);
}
update_nf_queue_handler();
// Binding this socket to queue.
queue_h = nfq_create_queue(nfq_h, 0, &handle_packets, NULL);
if (!queue_h) {
log_error("Error during nfq_create_queue()");
exit(1);
}
// Setting copy_packet mode.
if (nfq_set_mode(queue_h, NFQNL_COPY_PACKET, 0xffff) < 0) {
log_error("nfq_set_mode error");
exit(1);
}
return nfq_fd(nfq_h);
}
// This function reads the packets received on the fd
// @param fd is the pointer to the file descriptor to read on
static void url_filter_recv(struct uloop_fd *fd, unsigned int events)
int rv = (int)recv(fd->fd, buf, sizeof(buf), 0);
if (rv >= 0) {
nfq_handle_packet(nfq_h, buf, rv);
static int register_nfqueue_fd(void)
memset(&uloop_fd, 0, sizeof(struct uloop_fd));
uloop_fd.fd = open_netlink_connection();
uloop_fd.cb = url_filter_recv;
if (uloop_fd_add(&uloop_fd, ULOOP_READ)) {
log_error("Could not register nfqueue FD");
return -1;
}
return 0;
}
static int call_bundle_sync_script(void)
{
pid_t pid = fork();
if (pid < 0) {
log_error("fork failed");
return 1;
}
if (pid == 0) { // Child process
char *const args[] = { "/lib/parentalcontrol/sync_bundles.sh", NULL };
execv(args[0], args);
log_error("execl failed");
_exit(127); // Exit with failure status
}
// Parent process does not wait
return 0;
}
static void config_reload_cb(struct ubus_context *ctx __attribute__((unused)),
struct ubus_event_handler *ev __attribute__((unused)),
const char *type __attribute__((unused)),
struct blob_attr *msg __attribute__((unused)))
{
// if service was enabled before, perform cleanup before next run
if (serv_enabled) {
uloop_fd_delete(&uloop_fd);
// Destroying the queue handle and closing nfq handle.
if (queue_h != NULL) {
nfq_destroy_queue(queue_h);
queue_h = NULL;
if (nfq_h != NULL) {
nfq_close(nfq_h);
nfq_h = NULL;
}
// Terminate the thread
int rc = pthread_cancel(ptid);
if (rc != 0 && rc != ESRCH) {
log_error("Failed to terminate helper thread. Daemon EXIT");
exit(1);
}
pthread_join(ptid, NULL); // Wait for thread to terminate
free_all(&bbfdm_ctx.ubus_ctx); // Free all allocated resources
// Call function to update the global structures.
if (read_config(&serv_enabled)) {
log_error("Failed to read config");
exit(1);
if (serv_enabled) { // parental control service enabled
if (register_bundle_update(&bbfdm_ctx.ubus_ctx) != 0) {
log_error("Failed to register bundle_update event");
exit(1);
}
call_bundle_sync_script();
// Create and add nfqueue FD in uloop
if (register_nfqueue_fd() != 0) {
log_error("uloop_fd_add failed");
exit(1);
}
// Start thread to update dns-cache
if (pthread_create(&ptid, NULL, &send_dns_queries, NULL) != 0) {
log_error("pthread creation failed, Daemon EXIT");
exit(1);
}
}
}
static int register_config_change(struct ubus_context *uctx)
{
int ret;
memset(&config_reload_ev, 0, sizeof(struct ubus_event_handler));
config_reload_ev.cb = config_reload_cb;
ret = ubus_register_event_handler(uctx, &config_reload_ev, "parentalcontrol.reload");
if (ret)
return -1;
return 0;
}
static void unregister_config_change(struct ubus_context *uctx)
{
// unregister ubus handler if it exists
if (uctx) {
ubus_unregister_event_handler(uctx, &config_reload_ev);
}
}
static void usage(char *prog)
{
fprintf(stderr, "Usage: %s [options]\n", prog);
fprintf(stderr, "\n");
fprintf(stderr, "options:\n");
fprintf(stderr, " -l <log level> As per syslog, 0 to 7\n");
fprintf(stderr, " -d <schema dm> Display the schema data model supported by micro-service\n");
fprintf(stderr, " -h <help> To print this help menu\n");
fprintf(stderr, "\n");
}
int main(int argc, char **argv)
{
int ch, dm_type = 0;
while ((ch = getopt(argc, argv, "hdl:")) != -1) {
switch (ch) {
case 'l':
log_level = (int)strtoul(optarg, NULL, 10);
if (log_level < 0 || log_level > 7)
log_level = 6;
break;
case 'd':
dm_type++;
break;
case 'h':
usage(argv[0]);
exit(0);
default:
break;
}
}
memset(&bbfdm_ctx, 0, sizeof(struct bbfdm_context));
bbfdm_ubus_set_service_name(&bbfdm_ctx, "parentalcontrol");
bbfdm_ubus_load_data_model(tDynamicObj);
if (dm_type > 0) {
int res = bbfdm_print_data_model_schema(&bbfdm_ctx, dm_type);
exit(res);
}
openlog("urlfilter", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);
// Call function to update the global structures.
log_error("failed to read config");
// Create a list of dns server IPs.
if (init_dns_server_list() != 0) {
log_error("error in initialising dns server ip");
}
if (bbfdm_ubus_regiter_init(&bbfdm_ctx)) {
log_error("Failed to register ubus");
if (register_config_change(&bbfdm_ctx.ubus_ctx) != 0) {
log_error("Failed to register reload event");
goto exit;
}
if (serv_enabled) { // parental control service enabled
if (register_bundle_update(&bbfdm_ctx.ubus_ctx) != 0) {
log_error("Failed to register bundle_update event");
goto exit;
}
call_bundle_sync_script();
if (register_nfqueue_fd() != 0) {
log_error("url filter: uloop_fd_add failed");
goto exit;
}
// Create a thread which would send out dns query
// for all the blacklisted urls to create a dns cache.
if (pthread_create(&ptid, NULL, &send_dns_queries, NULL) != 0) {
log_error("pthread create failed");
goto destroy;
}
// Main loop of urlfilter
uloop_run();
// Terminate the thread before exiting
if (serv_enabled) {
pthread_cancel(ptid);
}
destroy:
// Destroying the queue handle and closing nfq handle.
if (queue_h != NULL)
nfq_destroy_queue(queue_h);
if (nfq_h != NULL)
nfq_close(nfq_h);
free_all(&bbfdm_ctx.ubus_ctx); // Free all allocated resources
unregister_config_change(&bbfdm_ctx.ubus_ctx);
bbfdm_ubus_regiter_free(&bbfdm_ctx);