added
authorRalph Ronnquist <ralph.ronnquist@gmail.com>
Tue, 7 Sep 2021 13:24:09 +0000 (23:24 +1000)
committerRalph Ronnquist <ralph.ronnquist@gmail.com>
Tue, 7 Sep 2021 13:24:09 +0000 (23:24 +1000)
16 files changed:
Makefile [new file with mode: 0644]
README.html [new file with mode: 0644]
README.md [new file with mode: 0644]
bin/nfblocker [new file with mode: 0755]
bin/nfblocker.sh [new file with mode: 0755]
blocked/squid-ads.acl [new symlink]
blocked/squid-dating.acl [new symlink]
blocked/squid-gambling.acl [new symlink]
blocked/squid-malicious.acl [new symlink]
blocked/squid-porn.acl [new symlink]
blocked/squid-video.acl [new symlink]
blocked/youtube-google-videos.acl [new symlink]
src/cache.c [new file with mode: 0644]
src/database.c [new file with mode: 0644]
src/nfblocker.c [new file with mode: 0644]
src/ssl.txt [new file with mode: 0644]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..534f9f1
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,28 @@
+# Distribution Makefile for nfbuilder
+#
+
+ifeq ($(shell dpkg -l libnetfilter-queue-dev),)
+$(info "nfblocker requires the libnetfilter-queue-dev package")
+$(info "please install it before making")
+$(error "TERMINATED")
+endif
+
+INSTALLDIR=/usr/local/sbin
+USERCOMMAND = nfblocker.sh
+
+.PHONY: install clean
+
+default: install
+
+bin/nfblocker: $(wildcard src/*.c)
+       gcc -g -Wall -o $@ $^ -lnetfilter_queue
+
+# Installing the control script in $(INSTALLDIR)
+
+$(INSTALLDIR)/$(USERCOMMAND): bin/nfblocker.sh $(INSTALLDIR)
+       ln -sTf $$(readlink -f $<) $@
+
+install: bin/nfblocker $(INSTALLDIR)/$(USERCOMMAND)
+
+clean:
+       rm -f bin/nfblocker
diff --git a/README.html b/README.html
new file mode 100644 (file)
index 0000000..fb7af87
--- /dev/null
@@ -0,0 +1,84 @@
+<h1>Blacklist based domain name filtering</h1>
+
+<p>The <code>nfblocker</code> utility is a blacklist based network traffic filter
+for <code>iptables</code> via <code>libnetfilter-queue</code>. It applies to HTTP and SSL
+traffic for recognizing and dropping packets that are directed to
+blacklisted domain names.</p>
+
+<h2>Dendencies</h2>
+
+<p>Operationally <code>nfblocker</code> depends on the <code>libnetfilter-queue-dev</code> and
+<code>iptables</code> packages, and for building, you'll also need a C build
+environment including <code>make</code>.</p>
+
+<p>The blacklist format is that of squidblacklist.org, which you'll need
+to acquire separately.</p>
+
+<h2>Build and Install</h2>
+
+<p><code>nfblocker</code> is distributed in a tar file, which should be unpacked at
+its future residence; e.g., as /usr/local/src/nfblocker-1.0.0. Then
+<code>cd</code> into that directory and type:</p>
+
+<blockquote>
+  <p><code># make</code></p>
+</blockquote>
+
+<p>This will build the binary filter, and install the control script as
+<code>/usr/local/sbin/nfblocker.sh</code>. Edit the Makefile to install
+elsewhere.</p>
+
+<h2>Setup and Confguration</h2>
+
+<p>The residence has a directory <code>acl</code> that is intended to hold all
+available access control lists, and a directory <code>blocked</code> that should
+be set up with links to the access control list files to use. For
+example:</p>
+
+<blockquote>
+  <p><code># ( cd blocked &amp;&amp; ln -s ../acl/youtube-google-videos.acl )</code></p>
+</blockquote>
+
+<p>That command will set up <code>youtube-google-videos.acl</code> to be an included
+blacklist. Do the opposite to remove; for example:</p>
+
+<blockquote>
+  <p><code># rm blocked/youtube-google-videos.acl</code></p>
+</blockquote>
+
+<h2>Running</h2>
+
+<p>The <code>nfblocker</code> is started with the following command:</p>
+
+<blockquote>
+  <p><code># nfblocker.sh start</code></p>
+</blockquote>
+
+<p>With the <code>start</code> argument, the script adds appropriate <code>iptables</code>
+rules to use direct certain traffic to net-filter queue 99, and it
+starts a background process fot that filtering.</p>
+
+<blockquote>
+  <p><code># nfblocker.sh reload</code></p>
+</blockquote>
+
+<p>With the <code>reload</code> argument, the control script stops and restarts the
+filter without changing <code>iptables</code> rules.</p>
+
+<blockquote>
+  <p><code># nfblocker.sh stop</code></p>
+</blockquote>
+
+<p>With the <code>stop</code> argument, the control script removes the <code>iptables</code>
+rules and terminates the filtering process.</p>
+
+<h2>Technical Detail</h2>
+
+<p>The filtering uses the given lists of domain names for rejecting
+packets. It recognizes HTTP message headers and SSL certificate
+requests, from where it picks out the targeted domain name. If that
+name is blacklised or in a blacklisted domain, then the packet is
+rejected.</p>
+
+<p>The filtering also uses a fixed size decision cache, so that
+subsequent decisions for the same target can be made quickly.</p>
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..f950a79
--- /dev/null
+++ b/README.md
@@ -0,0 +1,72 @@
+# Blacklist based domain name filtering
+
+The `nfblocker` utility is a blacklist based network traffic filter
+for `iptables` via `libnetfilter-queue`. It applies to HTTP and SSL
+traffic for recognizing and dropping packets that are directed to
+blacklisted domain names.
+
+## Dendencies
+
+Operationally `nfblocker` depends on the `libnetfilter-queue-dev` and
+`iptables` packages, and for building, you'll also need a C build
+environment including `make`.
+
+The blacklist format is that of squidblacklist.org, which you'll need
+to acquire separately.
+
+## Build and Install
+
+`nfblocker` is distributed in a tar file, which should be unpacked at
+its future residence; e.g., as /usr/local/src/nfblocker-1.0.0. Then
+`cd` into that directory and type:
+
+> `# make`
+
+This will build the binary filter, and install the control script as
+`/usr/local/sbin/nfblocker.sh`. Edit the Makefile to install
+elsewhere.
+
+## Setup and Confguration
+
+The residence has a directory `acl` that is intended to hold all
+available access control lists, and a directory `blocked` that should
+be set up with links to the access control list files to use. For
+example:
+
+> `# ( cd blocked && ln -s ../acl/youtube-google-videos.acl )`
+
+That command will set up `youtube-google-videos.acl` to be an included
+blacklist. Do the opposite to remove; for example:
+
+> `# rm blocked/youtube-google-videos.acl`
+
+## Running
+
+The `nfblocker` is started with the following command:
+
+> `# nfblocker.sh start`
+
+With the `start` argument, the script adds appropriate `iptables`
+rules to use direct certain traffic to net-filter queue 99, and it
+starts a background process fot that filtering.
+
+> `# nfblocker.sh reload`
+
+With the `reload` argument, the control script stops and restarts the
+filter without changing `iptables` rules.
+
+> `# nfblocker.sh stop`
+
+With the `stop` argument, the control script removes the `iptables`
+rules and terminates the filtering process.
+
+## Technical Detail
+
+The filtering uses the given lists of domain names for rejecting
+packets. It recognizes HTTP message headers and SSL certificate
+requests, from where it picks out the targeted domain name. If that
+name is blacklised or in a blacklisted domain, then the packet is
+rejected.
+
+The filtering also uses a fixed size decision cache, so that
+subsequent decisions for the same target can be made quickly.
diff --git a/bin/nfblocker b/bin/nfblocker
new file mode 100755 (executable)
index 0000000..a9a9c8a
Binary files /dev/null and b/bin/nfblocker differ
diff --git a/bin/nfblocker.sh b/bin/nfblocker.sh
new file mode 100755 (executable)
index 0000000..661d8d0
--- /dev/null
@@ -0,0 +1,38 @@
+#!/bin/bash
+#
+# This is a control script for nfblocker.
+#
+
+if [ -z "$1" ] ; then
+    echo "start or stop?"
+    exit 1
+fi
+
+cd $(dirname $(readlink $0))
+
+function start_nfblocker() {
+    LOG=/var/log/nfblocker.$(date +%Y%m%d)
+    BLOCKED=( ../acl/*.acl )
+    echo "BLOCKING ${BLOCKED[@]}" >> $LOG
+    ./nfblocker ../blocked/*.acl >> $LOG 2>&1 &
+}
+
+TABLE=OUTPUT
+case $1 in
+    start)
+       iptables -I $TABLE -p tcp -j NFQUEUE --queue-num 99
+       pkill -x nfblocker
+       start_nfblocker
+       ;;
+    reload)
+       pkill -x nfblocker
+       start_nfblocker
+       ;;
+    stop)
+       iptables -D $TABLE -p tcp -j NFQUEUE --queue-num 99
+       pkill -x nfblocker
+       ;;
+    *)
+       echo "Use start, stop or reload" >&2
+       ;;
+esac
diff --git a/blocked/squid-ads.acl b/blocked/squid-ads.acl
new file mode 120000 (symlink)
index 0000000..905bd71
--- /dev/null
@@ -0,0 +1 @@
+../acl/squid-ads.acl
\ No newline at end of file
diff --git a/blocked/squid-dating.acl b/blocked/squid-dating.acl
new file mode 120000 (symlink)
index 0000000..6a7fd51
--- /dev/null
@@ -0,0 +1 @@
+../acl/squid-dating.acl
\ No newline at end of file
diff --git a/blocked/squid-gambling.acl b/blocked/squid-gambling.acl
new file mode 120000 (symlink)
index 0000000..fd170a7
--- /dev/null
@@ -0,0 +1 @@
+../acl/squid-gambling.acl
\ No newline at end of file
diff --git a/blocked/squid-malicious.acl b/blocked/squid-malicious.acl
new file mode 120000 (symlink)
index 0000000..b0b7f81
--- /dev/null
@@ -0,0 +1 @@
+../acl/squid-malicious.acl
\ No newline at end of file
diff --git a/blocked/squid-porn.acl b/blocked/squid-porn.acl
new file mode 120000 (symlink)
index 0000000..0710db6
--- /dev/null
@@ -0,0 +1 @@
+../acl/squid-porn.acl
\ No newline at end of file
diff --git a/blocked/squid-video.acl b/blocked/squid-video.acl
new file mode 120000 (symlink)
index 0000000..d0b480f
--- /dev/null
@@ -0,0 +1 @@
+../acl/squid-video.acl
\ No newline at end of file
diff --git a/blocked/youtube-google-videos.acl b/blocked/youtube-google-videos.acl
new file mode 120000 (symlink)
index 0000000..06f57c8
--- /dev/null
@@ -0,0 +1 @@
+../acl/youtube-google-videos.acl
\ No newline at end of file
diff --git a/src/cache.c b/src/cache.c
new file mode 100644 (file)
index 0000000..2afb37a
--- /dev/null
@@ -0,0 +1,45 @@
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+
+typedef struct _CacheEntry {
+    unsigned char *domain;
+    unsigned int ix;
+} CacheEntry;
+
+struct {
+    CacheEntry *table;
+    int size;
+} cache;
+
+int hash_code(unsigned char *domain) {
+    int i = 0;
+    for ( ; *domain; domain++ ) {
+       i += *domain;
+    }
+    return i % cache.size;
+}
+
+int lookup_cache(unsigned char *domain) {
+    if ( cache.table ) {
+       int i = hash_code( domain );
+       if ( cache.table[i].domain &&
+            strcmp( (char*)  domain, (char*) cache.table[i].domain ) == 0 ) {
+           return cache.table[i].ix;
+       }
+    }
+    return -1;
+}
+
+void add_cache(unsigned char *domain,unsigned int ix) {
+    if ( cache.table == 0 ) {
+       cache.size = 1024;
+       cache.table = (CacheEntry*) calloc( cache.size, sizeof( CacheEntry ) );
+    }
+    int i = hash_code( domain );
+    if ( cache.table[i].domain ) {
+       free( cache.table[i].domain );
+    }
+    cache.table[i].domain =  (unsigned char*) strdup( (char*) domain );
+    cache.table[i].ix = ix;
+}
diff --git a/src/database.c b/src/database.c
new file mode 100644 (file)
index 0000000..02bb8dc
--- /dev/null
@@ -0,0 +1,280 @@
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+/**
+ * This file implements a "database" of "bad" domains, loaded from
+ * ".acl" files of a fairly strict format; each domain to block is
+ * written on a line starting with a period, immediately followed by
+ * the domain to block, then an optional comment.
+ *
+ * The database is populated by using the call sequence:
+ * 1. start_domain_database_loading();
+ * 2. load_domains( filename ); // repeated
+ * N. end_domain_database_loading();
+ *
+ * The final call triggers a reordering of domains so as to support
+ * binary search in reverse text order, for matching domain suffixes.
+ * See the function `tail_compare` for details.
+ */
+
+/**
+ * This is the Entry type for the "database", which basically is an
+ * array of these. The domain pointer will point at a domain name in
+ * the loaded ".acl" file, and length is the domain name length.
+ */
+typedef struct _Entry {
+    int length;
+    unsigned char *domain;
+} Entry;
+
+/**
+ * This is the domain name database root structure. It holds a pointer
+ * to the array of Entry records, the fill of that array, and the
+ * allocated size for that array (no lesser than the fill, of course).
+ */
+static struct {
+    Entry *table;
+    int fill;
+    int size;
+} database = { 0, 0, 0 };
+
+/**
+ * This function compares strings backwars; the last k bytes of string
+ * (a,na) versus string (b,nb). It also holds '.' as the least of
+ * characters, so as to ensure that refined/extended domain names are
+ * comparatively greater that their base domain names.
+ */
+static int tail_compare(unsigned char *a,unsigned char *b,int k) {
+    while ( k-- > 0 ) {
+       int c = *(--a) - *(--b);
+       if ( c != 0) {
+           if ( *a == '.' ) {
+               return -1;
+           }
+           if ( *b == '.' ) {
+               return 1;
+           }
+           return c;
+       }
+    }
+    return 0;
+}
+
+/**
+ * Extend the domain name table to allow additions.
+ */
+#define STARTSIZE 100000
+static void grow() {
+    if ( database.table ) {
+       Entry *old = database.table;
+       int s = database.size;
+       database.size += 100000;
+       database.table = (Entry*) calloc( database.size, sizeof( Entry ) );
+       memcpy( database.table, old, s * sizeof( Entry ) );
+       free( old );
+    } else {
+       database.table = (Entry*) calloc( STARTSIZE, sizeof( Entry ) );
+       database.size = STARTSIZE;
+    }
+}
+
+/**
+ * Determine the index for given domain. This matches computes a tail
+ * match between the given domain and the databse domains, returning
+ * the index for the matching database entry, or (-index-1) to
+ * indicate insertion point. In lookup mode, a database entry being a
+ * tail domain part of the given domain is also considered a match.
+ */
+static int index_domain(unsigned char *domain,int n,int lookup) {
+    int lo = 0;
+    int hi = database.fill;
+    while ( lo < hi ) {
+       int m = ( lo + hi ) / 2;
+       Entry *p = &database.table[ m ];
+       int k = p->length;
+       if ( n < k ) {
+           k = n;
+       }
+       int q = tail_compare( p->domain + p->length, domain + n, k );
+#if 0
+       fprintf( stderr, "%s %d %d %d\n", domain, k, m, q );
+#endif
+       if ( q == 0 ) {
+           if ( p->length < n ) {
+               // table entry shorter => new entry after, or match on lookup
+               if ( lookup && *(domain+n-k-1) == '.' ) {
+                   return m;
+               }
+               lo = m + 1;
+           } else if ( p->length > n ) {
+               // table entry longer  => new entry before
+               hi = m;
+           } else {
+               // equal
+               return m;
+           }
+       } else if ( q < 0 ) {
+           // new entry after
+           lo = m + 1;
+       } else {
+           // new entry before
+           hi = m;
+       }
+    }
+    return -lo - 1;
+}
+
+/**
+ * Determine the length of a "word"
+ */
+static int wordlen(unsigned char *p) {
+    unsigned char *q = p;
+    while ( *q > ' ' ) {
+       q++;
+    }
+    return q - p;
+}
+
+#if 0
+static void add_domain(char *domain) {
+    if ( database.fill >= database.size ) {
+       grow();
+    }
+    int length = wordlen( domain );
+    int i = index_domain( domain, length, 0 );
+    if ( i < 0 ) {
+       i = -i-1;
+       int tail = database.fill - i;
+       if ( tail ) {
+           memmove( &database.table[ i+1 ],
+                    &database.table[i],
+                    tail * sizeof( Entry ) );
+       }
+       database.table[ i ].domain = domain;
+       database.table[ i ].length = length;
+       database.fill++;
+    } else {
+       char *p1 = strndup( domain, length );
+       char *p2 = strndup( database.table[i].domain,
+                           database.table[i].length );
+       fprintf( stderr, "fill = %d %d %s  == %s\n",
+                i, database.fill, p1, p2 );
+       free( p1 );
+       free( p2 );
+    }
+}
+#endif 
+
+static void fast_add_domain(unsigned char *domain,int length) {
+    int fill = database.fill;
+    if ( fill >= database.size ) {
+       grow();
+    }
+    database.table[ fill ].length = length;
+    database.table[ fill ].domain = domain;
+    database.fill++;
+}
+
+static int table_order(Entry *a,Entry *b) {
+    int k = ( a->length < b->length )? a->length : b->length;
+    int c = tail_compare( a->domain + a->length,
+                         b->domain + b->length, k );
+    if ( c != 0 ) {
+       return c;
+    }
+    return a->length - b->length;
+}
+
+/**
+ * External call to check a given domain.
+ */
+unsigned int check_domain(unsigned char *domain) {
+    int i = index_domain( domain, wordlen( domain ), 1 );
+    return ( i < 0 )? 0 : ( i + 1 );
+}
+
+void start_domain_database_loading(void) {
+}
+
+#if 0
+static void dump_table() {
+    fprintf( stderr, "Table fill=%d size=%d\n", database.fill, database.size );
+    int i = 0;
+    for ( ; i < database.fill; i++ ) {
+       char *p = strndup( database.table[i].domain,
+                          database.table[i].length );
+       fprintf( stderr, "[%d] %d %p %s\n",
+                i, database.table[i].length, database.table[i].domain, p );
+       free( p );
+    }
+}
+#endif
+
+void end_domain_database_loading(void) {
+    qsort( database.table, database.fill, sizeof( Entry ),
+          (__compar_fn_t) table_order );
+    //dump_table();
+}
+
+/**
+ * Load BAD domain names from file. The file is line based where data
+ * lines consist of domain name starting with period and ending with
+ * space or newline, and other lines ignored.
+ */
+void load_domains(char *file) {
+    struct stat info;
+    unsigned char *data;
+    //fprintf( stderr, "state(\"%s\",&info)\n", file );
+    if ( stat( file, &info ) ) {
+       perror( file );
+       exit( 1 );
+    }
+    int n = info.st_size;
+    data = (unsigned char *) malloc( n );
+    //fprintf( stderr, "open(\"%s\",)\n", file );
+    int fd = open( file, O_RDONLY );
+    if ( fd < 0 ) {
+       perror( file );
+       exit( 1 );
+    }
+    //fprintf( stderr, "Loading %s\n", file );
+    unsigned char *end = data;
+    while ( n > 0 ) {
+       int k = read( fd, end, n );
+       if ( k == 0 ) {
+           fprintf( stderr, "Premature EOF for %s\n", file );
+           exit( 1 );
+       }
+       end += k;
+       n -= k;
+    }
+    //fprintf( stderr, "processing %s %p %p\n", file, data, end );
+    unsigned char *p = data;
+#if 0
+    int count = 0;
+#endif
+    while( p < end ) {
+#if 0
+       if ( ( ++count % 10000 ) == 0 ) {
+           fprintf( stderr, "%d rules\n", count );
+       }
+#endif
+       if ( *p == '.' ) {
+           unsigned char *domain = ++p;
+           while ( *p > ' ' ) {
+               p++;
+           }
+           fast_add_domain( domain, p - domain );
+       }
+       while ( p < end && *p != '\n' ) {
+           p++;
+       }
+       p++;
+    }
+    close( fd );
+}
diff --git a/src/nfblocker.c b/src/nfblocker.c
new file mode 100644 (file)
index 0000000..b8b979c
--- /dev/null
@@ -0,0 +1,286 @@
+#include <linux/types.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/tcp.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <linux/netfilter.h>            /* for NF_ACCEPT */
+
+#include <libnetfilter_queue/libnetfilter_queue.h>
+
+// Caching of verdicts
+unsigned int lookup_cache(unsigned char *domain);
+void add_cache(unsigned char *domain,unsigned int ix);
+int hash_code(unsigned char *domain);
+
+// BAD domains database
+unsigned int check_domain(unsigned char *domain);
+void load_domains(char *file);
+void start_domain_database_loading(void);
+void end_domain_database_loading(void);
+
+/**
+ * Return packet id, or 0 on error.
+ */
+static u_int32_t get_packet_id(struct nfq_data *tb) {
+    struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr( tb );
+    return ( ph )? ntohl( ph->packet_id ) : 0;
+}
+
+// Payload headers
+struct headers {
+    struct ip first;
+    struct tcphdr second;
+    //unsigned char pad[12]; // ??
+};
+
+///////// Debugging
+
+static unsigned char *tell_ip(u_int32_t ip) {
+    static unsigned char THEIP[20];
+    unsigned char *b = (unsigned char *)&ip;
+    sprintf( (char*) THEIP, "%d.%d.%d.%d%c", b[0], b[1], b[2], b[3], 0 );
+    return THEIP;
+}
+
+static u_int32_t get_dest_ip4(unsigned char *data) {
+    struct headers *p = (struct headers *) data;
+    return p->first.ip_dst.s_addr;
+}
+
+/**
+ * Review payload packet payload
+ */
+static void view_payload(unsigned char *data,int length) {
+    u_int32_t ip4 = get_dest_ip4( data );
+    u_int16_t port = ntohs( ((struct headers *) data )->second.th_dport );
+    u_int8_t syn = sizeof( struct headers );
+    unsigned char *body = data ;//+ sizeof( struct headers );
+#define END 400
+    unsigned char * end = body + ( ( length > END )? END : length );
+    fprintf( stderr, "%s %d %d %d ", tell_ip( ip4 ), syn, port, length );
+    while ( body < end ) {
+       unsigned char c = *body++;
+       if ( c < ' ' || c >= 127 || 1 ) {
+           fprintf( stderr, "%02x ", c );
+       } else {
+           fprintf( stderr, "%c", c );
+       }
+    }
+    fprintf( stderr, "\n" );
+}
+
+//////////////////
+static unsigned char buffer[1000];
+
+/**
+ * SSL traffic includes a data packet with a clear text host name.
+ * This is knwon as the SNI extension.
+ */
+static unsigned char *ssl_host(unsigned char *data,int length) {
+    // Check that it's a "Client Hello" message
+    unsigned char *p = data + sizeof( struct headers ) + 12;
+    if ( p[0] != 0x16 || p[1] != 0x03 || p[5] != 0x01 || p[6] != 0x00 ) {
+       return 0;
+    }
+    //fprintf( stderr, "Client Hello\n" );
+    // Note minor version p[2] is not checked
+    // record_length = 256 * p[3] + p[4]
+    // handshake_message_length = 256 * p[7] + p[8]
+    if ( p[9] != 0x03 || p[10] != 0x03 ) { // TLS 1.2 (?ralph?)
+       return 0;
+    }
+    //fprintf( stderr, "TLS 1.2\n" );
+    unsigned int i = 46 + ( 256 * p[44] ) + p[45];
+    i += p[i] + 1;
+    unsigned int extensions_length = ( 256 * p[i] ) + p[i+1];
+    i += 2;
+    int k = 0;
+    //fprintf( stderr, "TLS 1.2 %d %d\n", i, extensions_length );
+    while ( k < extensions_length ) {
+       unsigned int type = ( 256 * p[i+k] ) + p[i+k+1];
+       k += 2;
+       unsigned int length = ( 256 * p[i+k] ) + p[i+k+1];
+       k += 2;
+       //fprintf( stderr, "Extension %d %d\n", k-4, type );
+       if ( type == 0 ) { // Server Name
+           if ( p[i+k+2] ) {
+               break; // Name badness
+           }
+           unsigned int name_length = ( 256 * p[i+k+3] ) + p[i+k+4];
+           unsigned char *path = &p[i+k+5];
+           memcpy( buffer, path, name_length );
+           buffer[ name_length ] = '\0';
+           return buffer;
+       }
+       k += length;
+    }
+    // This point is only reached on "missing or bad SNI".
+    view_payload( data, length );
+    return 0;
+}
+
+/**
+ * HTTP traffic includes a data packet with the host name as a
+ * "Host:" attribute.
+ */
+static unsigned char *http_host(unsigned char *data,int length) {
+    unsigned char *body = data + sizeof( struct headers );
+    if ( ( strncmp( (char*) body, "GET ", 4 ) != 0 ) &&
+        ( strncmp( (char*) body, "POST ", 5 ) != 0 ) ) {
+       return 0;
+    }
+    unsigned char *end = data + length - 6;
+    int check = 0;
+    for ( ; body < end; body++ ) {
+       if ( check ) {
+           if ( strncmp( (char*) body, "Host:", 5 ) == 0 ) {
+               body += 5;
+               for( ; body < end; body++ ) if ( *body != ' ' ) break;
+               unsigned char *start = body;
+               int n = 0;
+               for( ; body < end; n++, body++ ) if ( *body <= ' ' ) break;
+               if ( n < 5 ) {
+                   return 0;
+               }
+               memcpy( buffer, start, n );
+               buffer[ n ] = '\0';
+               return buffer;
+           }
+           if ( strncmp( (char*) body, "\r\n", 2 ) == 0 ) {
+               return 0;
+           }
+           for( ; body < end; body++ ) if ( *body == '\n' ) break;
+           if ( body >= end ) {
+               return 0;
+           }
+       }
+       check = ( *body == '\n' );
+    }
+    return 0;
+}
+
+/**
+ * Callback function to handle a packet.
+ */
+static int cb(
+    struct nfq_q_handle *qh,
+    struct nfgenmsg *nfmsg,
+    struct nfq_data *nfa, void *code )
+{
+    u_int32_t id = get_packet_id( nfa );
+    unsigned char *data;
+    int length = nfq_get_payload( nfa, &data);
+    int verdict = NF_ACCEPT;
+    u_int32_t ip4 = get_dest_ip4( data );
+#if 0
+    fprintf( stderr, "PKT %s %d\n", tell_ip( ip4 ), length );
+#endif
+    if ( length >= 100 ) {
+       unsigned char *host = http_host( data, length );
+#if 1
+           fprintf( stderr, "HTTP HOST %s %s\n", tell_ip( ip4 ), host );
+#endif
+       if ( host == 0 ) {
+           host = ssl_host( data, length );
+#if 1
+           fprintf( stderr, "SSL HOST %s %s\n", tell_ip( ip4 ), host );
+#endif
+       }
+       if ( host ) {
+           int i = lookup_cache( host );
+           if ( i < 0 ) {
+               unsigned int ix = check_domain( host );
+               add_cache( host, ix );
+#if 1
+               fprintf( stderr, "%s %d %s ** %d\n",
+                        tell_ip( ip4 ), hash_code( host ), host, ix );
+#endif
+               if ( ix > 0 ) {
+                   verdict = NF_DROP;
+               }
+           } else if ( i > 0 ) {
+               verdict = NF_DROP;
+           }
+       }
+    }
+    return nfq_set_verdict(qh, id, verdict, 0, NULL);
+}
+
+/**
+ * Program main function.
+ */
+int main(int argc, char **argv) {
+    // Load the database
+    start_domain_database_loading();
+    int n = 1;
+    for ( ; n < argc; n++ ) {
+       fprintf( stderr, "Loading blacklist %s\n", argv[ n ] );
+       load_domains( argv[ n ] );
+    }
+    end_domain_database_loading();
+    
+    struct nfq_handle *h;
+    struct nfq_q_handle *qh;
+    //struct nfnl_handle *nh;
+    int fd;
+    int rv;
+    char buf[4096] __attribute__ ((aligned));
+    
+    fprintf( stderr, "opening library handle\n");
+    h = nfq_open();
+    if ( !h ) {
+       fprintf(stderr, "error during nfq_open()\n");
+       exit(1);
+    }
+    
+    fprintf( stderr, "unbinding any existing nf_queue handler\n" );
+    if ( nfq_unbind_pf(h, AF_INET) < 0 ) {
+       fprintf(stderr, "error during nfq_unbind_pf()\n");
+       exit(1);
+    }
+    
+    fprintf( stderr, "binding nfnetlink_queue as nf_queue handler\n" );
+    if ( nfq_bind_pf(h, AF_INET) < 0 ) {
+       fprintf(stderr, "error during nfq_bind_pf()\n");
+       exit(1);
+    }
+
+#define THEQUEUE 99
+    fprintf( stderr, "binding this socket to queue '%d'\n", THEQUEUE );
+    qh = nfq_create_queue( h,  THEQUEUE, &cb, NULL );
+    if ( !qh ) {
+       fprintf(stderr, "error during nfq_create_queue()\n");
+       exit(1);
+    }
+    
+    fprintf( stderr, "setting copy_packet mode\n" );
+    if ( nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff ) < 0) {
+       fprintf(stderr, "can't set packet_copy mode\n");
+       exit(1);
+    }
+    
+    fd = nfq_fd( h );
+    
+    while ( ( rv = recv(fd, buf, sizeof(buf), 0) ) && rv >= 0 ) {
+       //printf( "pkt received\n" );
+       nfq_handle_packet(h, buf, rv);
+    }
+    
+    fprintf( stderr, "unbinding from queue %d\n", THEQUEUE);
+    nfq_destroy_queue(qh);
+    
+#ifdef INSANE
+    /* normally, applications SHOULD NOT issue this command, since it
+       detaches other programs/sockets from AF_INET, too ! */
+    fprintf( stderr, "unbinding from AF_INET\n");
+    nfq_unbind_pf(h, AF_INET);
+#endif
+    
+    fprintf( stderr, "closing library handle\n");
+    nfq_close( h );
+    
+    exit( 0 );
+}
diff --git a/src/ssl.txt b/src/ssl.txt
new file mode 100644 (file)
index 0000000..9dbe988
--- /dev/null
@@ -0,0 +1,42 @@
+const unsigned char good_data_2[] = {
+    // TLS record
+    0x16, // [0] Content Type: Handshake
+    0x03, 0x01, // [1,2] Version: TLS 1.0
+    0x00, 0x6c, // [3,4] Length (use for bounds checking)
+        // Handshake
+        0x01, // [5] Handshake Type: Client Hello
+        0x00, 0x00, 0x68, // [6,7,8] Length (use for bounds checking)
+        0x03, 0x03, // [9,10] Version: TLS 1.2
+        // [11,,42] Random (32 bytes fixed length)
+        0xb6, 0xb2, 0x6a, 0xfb, 0x55, 0x5e, 0x03, 0xd5,
+        0x65, 0xa3, 0x6a, 0xf0, 0x5e, 0xa5, 0x43, 0x02,
+        0x93, 0xb9, 0x59, 0xa7, 0x54, 0xc3, 0xdd, 0x78,
+        0x57, 0x58, 0x34, 0xc5, 0x82, 0xfd, 0x53, 0xd1,
+        0x00, // [43] Session ID Length (skip past this much)
+        0x00, 0x04, // [44,45] Cipher Suites Length (skip past this much)
+            0x00, 0x01, // NULL-MD5
+            0x00, 0xff, // RENEGOTIATION INFO SCSV
+        0x01, // Compression Methods Length (skip past this much)
+            0x00, // NULL
+        0x00, 0x3b, // Extensions Length (use for bounds checking)
+            // Extension
+            0x00, 0x00, // Extension Type: Server Name (check extension type)
+            0x00, 0x0e, // Length (use for bounds checking)
+            0x00, 0x0c, // Server Name Indication Length
+                0x00, // Server Name Type: host_name (check server name type)
+                0x00, 0x09, // Length (length of your data)
+                // "localhost" (data your after)
+                0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74,
+            // Extension
+            0x00, 0x0d, // Extension Type: Signature Algorithms (check extension type)
+            0x00, 0x20, // Length (skip past since this is the wrong extension)
+            // Data
+            0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03,
+            0x05, 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01,
+            0x04, 0x02, 0x04, 0x03, 0x03, 0x01, 0x03, 0x02,
+            0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03,
+            // Extension
+            0x00, 0x0f, // Extension Type: Heart Beat (check extension type)
+            0x00, 0x01, // Length (skip past since this is the wrong extension)
+            0x01 // Mode: Peer allows to send requests
+};