diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 361d457..93c67d1 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -102,6 +102,9 @@
         env:
           srcdir: "${{ github.workspace }}/tests/unit_tests/openvpn"
 
+      - name: Run reflect unit test
+        run: ./unittests/test_reflect.exe
+
   ubuntu:
     strategy:
       fail-fast: false
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ad620fa..5aba2d4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -369,6 +369,7 @@
     src/openvpn/base64.c
     src/openvpn/base64.h
     src/openvpn/basic.h
+    src/openvpn/bloom.c
     src/openvpn/buffer.c
     src/openvpn/buffer.h
     src/openvpn/circ_list.h
@@ -511,6 +512,8 @@
     src/openvpn/shaper.h
     src/openvpn/sig.c
     src/openvpn/sig.h
+    src/openvpn/siphash.h
+    src/openvpn/siphash_reference.c
     src/openvpn/socket.c
     src/openvpn/socket.h
     src/openvpn/socks.c
@@ -605,6 +608,7 @@
         "test_packet_id"
         "test_pkt"
         "test_provider"
+        "test_reflect"
         "test_ssl"
         "test_user_pass"
         )
@@ -770,6 +774,19 @@
         src/openvpn/session_id.c
         )
 
+    target_sources(test_reflect PRIVATE
+        src/openvpn/bloom.c
+        src/openvpn/reflect_filter.c
+        src/openvpn/reflect_filter.h
+        src/openvpn/siphash.h
+        src/openvpn/siphash_reference.c
+        src/openvpn/otime.c
+        src/openvpn/crypto_mbedtls.c
+        src/openvpn/crypto_openssl.c
+        src/openvpn/crypto.c
+        src/openvpn/packet_id.c
+        )
+
     target_sources(test_pkt PRIVATE
         tests/unit_tests/openvpn/mock_win32_execve.c
         src/openvpn/argv.c
diff --git a/Changes.rst b/Changes.rst
index 439352a..af72678 100644
--- a/Changes.rst
+++ b/Changes.rst
@@ -9,6 +9,14 @@
     the user experience as the client shows an error instead of running into
     a timeout when the server just stops responding completely.
 
+Bloom filter based reflection protection
+    To avoid the limitation of ``--connect-freq-initial`` to block legimitate
+    clients when an OpenVPN server is tried to be used in a reflection attack,
+    the new ``--connect-freq-initial-bloom-limit`` can impose limit on a
+    per-subnet basis. See the manual for more details. Note: this option
+    requires 2MB of extra memory in the default configuration.
+
+
 Deprecated features
 -------------------
 ``secret`` support has been removed by default.
diff --git a/doc/man-sections/server-options.rst b/doc/man-sections/server-options.rst
index 0632e31..f102df2 100644
--- a/doc/man-sections/server-options.rst
+++ b/doc/man-sections/server-options.rst
@@ -208,6 +208,77 @@
   will not be counted against the limit. The default is to allow
   100 initial connection per 10s.
 
+
+--connect-freq-initial-bloom-size args
+
+   Valid syntax:
+   ::
+
+      connect-freq-initial-bloom-size size [numhashes]
+
+   Configures the size of the bloom filter used for the initial connection
+   packet responses. See `--connect-freq-initial-bloom-limit` for a full
+   description of the feature.
+
+   Bloom filters are probabilistic by their nature and the larger the size
+   of the bloom filter is, the more accurate they are. This option configures
+   the number of buckets in the bloom filter. Each bucket takes up 4 bits.
+   E.g. the default of  8336608 (8 * 2^20) when the option is not set
+   will require 2MB of memory. The numhashes will control the number of hash
+   functions that are used for the bloom filter. The default is 7.
+
+--connect-freq-initial-bloom-limit arg
+
+   Valid syntax:
+   ::
+
+      connect-freq-initial-bloom-limit inet netmask limit
+      connect-freq-initial-bloom-limit inet6 netmask limit
+
+   This option implements a rate limiting function for initial packet like
+   ``--connect-freq-initial`` but takes the source of the initial packet into
+   account. This function shares the the reset after a time period and
+   enabling this function will not disable the overall limit of
+   ``--conect-freq-initial``.
+
+   To avoid resource exhaustion on the OpenVPN server side from reflection
+   attacks and also trying to avoid blocking legitimate clients when an
+   attacker tries to use an OpenVPN server, the limits are not applied globally
+   but rather a per netmask granularity. Each line of this command configures
+   a separate granularity. As example, the following configuration,
+
+   ::
+
+        connect-freq-initial 1000 60
+        connect-freq-initial-bloom-limit inet 24 50
+        connect-freq-initial-bloom-limit inet 8 100
+        connect-freq-initial-bloom-limit inet6 56 50
+        connect-freq-initial-bloom-limit inet6 32 100
+
+    will limit the number of (unanswered) replies per /24 network to 50
+    packets. After 50 packets from the same /24, the server will
+    drop any further packets. Similarly, if a /16 network receives more than
+    100 connection requests, the server will drop further packets.
+    If the count of all packets exceeds 1000, further packets will be
+    completely dropped regardless of their origin.
+
+   The implementation uses a counting bloom filter to store information of how
+   many packets the server has seen per subnet. A bloom filter is a
+   probabilistic data structure can yield false positives. In this case, the
+   server will drop packets even though the limit
+   for that particular subnet is not yet reached. Since we are using a
+   counting bloom filter, a reply from a client that triggers a removal of an
+   entry can also lead to a false negative. However, this is limited in the
+   sense a reply can at most trigger one false negative.
+
+   Every limit configured with this option will put more more entries into
+   the bloomfilter since per limit (per family), one entry is put into the
+   bloom filter. So a large number of entries requires a larger bloom filter
+   to avoid too many false positives.
+
+   IPv6 mapped IPv4 addresses (::ffff:0:0/96) are treated as IPv4 addresses
+   for these limits.
+
 --duplicate-cn
   Allow multiple clients with the same common name to concurrently
   connect. In the absence of this option, OpenVPN will disconnect a client
diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am
index 3784a98..ca77718 100644
--- a/src/openvpn/Makefile.am
+++ b/src/openvpn/Makefile.am
@@ -44,6 +44,7 @@
 	auth_token.c auth_token.h \
 	base64.c base64.h \
 	basic.h \
+    bloom.c bloom.h \
 	buffer.c buffer.h \
 	circ_list.h \
 	clinat.c clinat.h \
@@ -124,6 +125,7 @@
 	session_id.c session_id.h \
 	shaper.c shaper.h \
 	sig.c sig.h \
+	siphash_reference.c siphash.h \
 	socket.c socket.h \
 	socks.c socks.h \
 	ssl.c ssl.h  ssl_backend.h \
diff --git a/src/openvpn/bloom.c b/src/openvpn/bloom.c
new file mode 100644
index 0000000..729429e
--- /dev/null
+++ b/src/openvpn/bloom.c
@@ -0,0 +1,253 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2022 OpenVPN Inc <sales@openvpn.net>
+ *
+ *  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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/* This file implements a specialised counting bloom filter implementation
+ * used in OpenVPN to mitigate it being used in reflection attacks. The bloom
+ * implementation should be general enough to be used in other contexts
+ * however */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#elif defined(_MSC_VER)
+#include "config-msvc.h"
+#endif
+
+#include "syshead.h"
+
+
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include "siphash.h"
+#include "crypto.h"
+
+#include "bloom.h"
+
+
+static size_t
+calc_ceil_log2(size_t num)
+{
+    /* Figure out how many bits we have in our hash by doing
+     * ceil(log2(bf->size) */
+
+    size_t numbits = 0;
+    while (num >>= 1)
+    {
+        numbits++;
+    }
+    return numbits;
+}
+
+/**
+ * Calculate number of bytes needed for each hash function in the bloom filter
+ *
+ */
+static inline size_t
+calculate_num_bytes_hashfun(size_t size)
+{
+    size_t hash_bits = calc_ceil_log2(size);
+    /* round up to the nearest byte size */
+    size_t hash_bytes = (hash_bits + 7) /8;
+    return hash_bytes;
+}
+
+/**
+ * A bloom filter uses a number of hashes to perform its functionality.
+ *
+ * Instead of using the same number of siphash function we split the bytes of
+ * the siphash to the different hashes.
+ *
+ * We could also optimise this further by splitting at the bit level but that
+ * crates a lot of extra code for bit shifting, etc. so we waste some bits
+ *
+ * @param bf
+ * @param num_hashes
+ * @return
+ */
+size_t
+calculate_num_sip_hash_hashes(struct bloom_filter *bf)
+{
+    size_t hash_bytes = calculate_num_bytes_hashfun(bf->size);
+    size_t total_bytes = bf->num_hashes * hash_bytes;
+
+    /* Round up to next SIPHASH_HASH_SIZE */
+    size_t num_siphash = (total_bytes + SIPHASH_HASH_SIZE -1) / SIPHASH_HASH_SIZE;
+
+    return num_siphash;
+}
+
+/**
+ * Calculates the number of bytes we need for storing a bloom filter of size
+ * size. We add + 1 to avoid rounding problems and too small allocation */
+static inline
+size_t
+bloom_get_filter_byte_count(size_t size)
+{
+    static_assert(sizeof(bloom_counter_t) * 8 % BLOOM_FILTER_BITS_COUNT == 0,
+                  "bloom_counter_t must be a multiple of BLOOM_FILTER_BIT_COUNT");
+
+    return size * sizeof(bloom_counter_t)/BLOOM_FILTER_BITS_COUNT + 1;
+}
+
+
+static inline
+size_t
+bloom_get_filter_bit_offset(size_t bucket)
+{
+    return (bucket * BLOOM_FILTER_BITS_COUNT) % sizeof(bloom_counter_t);
+}
+
+static inline
+size_t
+bloom_get_filter_array_index(size_t bucket)
+{
+    return (bucket * BLOOM_FILTER_BITS_COUNT) / sizeof(bloom_counter_t);
+}
+
+static inline bloom_counter_t
+bloom_get_filter_get_counter(struct bloom_filter *bf, size_t bucket)
+{
+    static_assert((BLOOM_FILTER_BITS_MASK + 1) ==  (1 << BLOOM_FILTER_BITS_COUNT),
+                  "BLOOM_FILTER_BITMASK and BLOOM_FILTER_BIT_COUNT are inconsistent");
+
+    bloom_counter_t counter = bf->buckets[bloom_get_filter_array_index(bucket)];
+    size_t bitoffset = bloom_get_filter_bit_offset(bucket);
+
+    return (counter >> bitoffset) & BLOOM_FILTER_BITS_MASK;
+}
+
+static inline void
+bloom_set_filter_counter(struct bloom_filter *bf, size_t bucket, bloom_counter_t value)
+{
+    bloom_counter_t data = bf->buckets[bloom_get_filter_array_index(bucket)];
+    size_t bitoffset = bloom_get_filter_bit_offset(bucket);
+
+    data = data & ~(BLOOM_FILTER_BITS_MASK << bitoffset);
+
+    data = data | ((value & BLOOM_FILTER_BITS_MASK) << bitoffset);
+
+    bf->buckets[bloom_get_filter_array_index(bucket)] = data;
+}
+
+/**
+ * Creates a new bloom filter structure
+ * @param size the number of buckets.
+ * @return  the newly created bloom filter structure
+ */
+struct bloom_filter *
+bloom_create(size_t size, size_t num_hashes, struct gc_arena *gc)
+{
+    size_t bloomfilter_bytes = bloom_get_filter_byte_count(size);
+    struct bloom_filter *bf = gc_malloc(sizeof(struct bloom_filter) + bloomfilter_bytes,
+                                        false, gc);
+    bf->size = size;
+    bf->num_hashes = num_hashes;
+
+    bf->hash_bytes = calculate_num_bytes_hashfun(size);
+    bf->num_siphash = calculate_num_sip_hash_hashes(bf);
+
+    ALLOC_ARRAY_GC(bf->siphash_keys, struct siphash_key, bf->num_siphash, gc);
+
+    bloom_clear(bf);
+    return bf;
+}
+
+/**
+ * Clear the bloom filter, making it empty again as if it were freshly created
+ * @param bf the bloom structure to clear
+ */
+void
+bloom_clear(struct bloom_filter *bf)
+{
+    memset(bf->buckets, 0, bloom_get_filter_byte_count(bf->size));
+
+    /* We randomise the bloom filter keys on every clear of the bloom filter
+     * to avoid scenarios where an attacker might learn specific pattern
+     * that could exploit false positives in the bloom filter */
+    for (size_t i = 0; i < bf->num_siphash; i++)
+    {
+        prng_bytes(bf->siphash_keys[i].key, SIPHASH_KEY_SIZE);
+    }
+}
+
+
+static bloom_counter_t
+bloom_add_test(struct bloom_filter *bf, const uint8_t *item, size_t len, bloom_counter_t inc)
+{
+    uint8_t result[SIPHASH_HASH_SIZE];
+    size_t j = 0;
+    size_t idx = 0;
+    bloom_counter_t ret = bloom_counter_max;
+
+    for (size_t i = 0; i < bf->num_hashes; i++)
+    {
+        size_t bucket = 0;
+        for (int k = 0; k < bf->hash_bytes; k++)
+        {
+            if (idx == 0)
+            {
+                /* We have no longer unused bytes in result, generate the next hash */
+                siphash(item, len, bf->siphash_keys[j++].key, result, SIPHASH_HASH_SIZE);
+            }
+
+            bucket = bucket << 8;
+            bucket |= result[idx];
+
+            idx = (idx + 1) % SIPHASH_HASH_SIZE;
+        }
+
+        bucket = bucket % bf->size;
+        bloom_counter_t value = bloom_get_filter_get_counter(bf, bucket);
+
+        ret = min_bloom_counter(ret, value);
+
+        if (inc)
+        {
+            value = min_bloom_counter(bloom_counter_max, value + 1);
+            bloom_set_filter_counter(bf, bucket, value);
+        }
+    }
+    return ret;
+}
+
+
+bloom_counter_t
+bloom_add(struct bloom_filter *bf, const uint8_t *item, size_t len)
+{
+    return bloom_add_test(bf, item, len, 1);
+}
+
+bloom_counter_t
+bloom_remove(struct bloom_filter *bf, const uint8_t *item, size_t len)
+{
+    return bloom_add_test(bf, item, len, -1);
+}
+
+
+bloom_counter_t
+bloom_test(struct bloom_filter *bf, const uint8_t *item, size_t len)
+{
+    return bloom_add_test(bf, item, len, 0);
+}
diff --git a/src/openvpn/bloom.h b/src/openvpn/bloom.h
new file mode 100644
index 0000000..e180261
--- /dev/null
+++ b/src/openvpn/bloom.h
@@ -0,0 +1,97 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single TCP/UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2022 OpenVPN Inc <sales@openvpn.net>
+ *
+ *  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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef BLOOM_H
+#define BLOOM_H
+
+#include <stdint.h>
+#include "siphash.h"
+#include "buffer.h"
+
+/* This is the type we use for the buckets. This is split into small buckets
+ * with BLOOM_FILTER_BIT_COUNT size */
+typedef uint32_t bloom_counter_t;
+#define BLOOM_FILTER_BITS_COUNT    2
+#define BLOOM_FILTER_BITS_MASK   0x03
+#define bloom_counter_max        0x03
+
+
+static inline bloom_counter_t
+min_bloom_counter(bloom_counter_t x, bloom_counter_t y)
+{
+    if (x < y)
+    {
+        return x;
+    }
+    else
+    {
+        return y;
+    }
+}
+
+struct siphash_key {
+    uint8_t key[SIPHASH_KEY_SIZE];
+};
+
+struct bloom_filter {
+    /** Size of the bloom filter in entries, ie total bits/bits per counter */
+    size_t size;
+
+    /** Number of bytes used by each hash function:
+     *
+     * log2(size * 8) bits rounded up to the next byte
+     *
+     * This is a cached value since log2 is surprisingly slow
+     * (5% of total time of if we do not cache it) */
+    size_t hash_bytes;
+
+    /** number of hashes we use to determine the bit positions */
+    size_t num_hashes;
+    /** number of siphash function needed to calculate. This can be
+     * calculated from the other members of the struct but we store it
+     * in the struct for fast access */
+    size_t num_siphash;
+
+    /** keys for the siphash functions */
+    struct siphash_key *siphash_keys;
+
+    /** the actual buckets that hold the data */
+    bloom_counter_t buckets[];
+};
+
+
+struct bloom_filter *
+bloom_create(size_t size, size_t num_hashes, struct gc_arena *gc);
+
+bloom_counter_t
+bloom_test(struct bloom_filter *bf, const uint8_t *item, size_t len);
+
+bloom_counter_t
+bloom_add(struct bloom_filter *bf, const uint8_t *item, size_t len);
+
+bloom_counter_t
+bloom_remove(struct bloom_filter *bf, const uint8_t *item, size_t len);
+
+void
+bloom_clear(struct bloom_filter *bf);
+#endif /* ifndef BLOOM_H */
diff --git a/src/openvpn/mudp.c b/src/openvpn/mudp.c
index 268b430..9fbffac 100644
--- a/src/openvpn/mudp.c
+++ b/src/openvpn/mudp.c
@@ -84,7 +84,7 @@
     {
         /* Check if we are still below our limit for sending out
          * responses */
-        if (!reflect_filter_rate_limit_check(m->initial_rate_limiter))
+        if (!reflect_filter_check(m->initial_rate_limiter, from))
         {
             return false;
         }
@@ -254,7 +254,8 @@
                 {
                     /* a successful three-way handshake only counts against
                      * connect-freq but not against connect-freq-initial */
-                    reflect_filter_rate_limit_decrease(m->initial_rate_limiter);
+                    reflect_filter_rate_limit_decrease(m->initial_rate_limiter,
+                                                       &m->top.c2.from.dest);
 
                     mi = multi_create_instance(m, &real);
                     if (mi)
diff --git a/src/openvpn/multi.c b/src/openvpn/multi.c
index 0509911..b9de040 100644
--- a/src/openvpn/multi.c
+++ b/src/openvpn/multi.c
@@ -368,7 +368,8 @@
     m->new_connection_limiter = frequency_limit_init(t->options.cf_max,
                                                      t->options.cf_per);
     m->initial_rate_limiter = initial_rate_limit_init(t->options.cf_initial_max,
-                                                      t->options.cf_initial_per);
+                                                      t->options.cf_initial_per,
+                                                      &t->options.initial_cf_bloom_config);
 
     /*
      * Allocate broadcast/multicast buffer list
diff --git a/src/openvpn/options.c b/src/openvpn/options.c
index 649f48b..20c953a 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -3461,6 +3461,22 @@
 }
 
 static void
+check_filter_tier_under_global_limit(struct filter_tier *ft, size_t limit,
+                                     const char *family)
+{
+    while (ft)
+    {
+        if (ft->limit < limit)
+        {
+            msg(M_WARN, "Note: --connect-freq-initial-bloom-limit %s %d %d "
+                "has a larger limit than connect-freq-limit-initial "
+                "limit (%zu).", family, ft->netmask, ft->limit, limit);
+        }
+        ft = ft->next;
+    }
+}
+
+static void
 options_postprocess_verify(const struct options *o)
 {
     if (o->connection_list)
@@ -3484,6 +3500,18 @@
             "channel offload: packets are always sent to the VPN "
             "interface and then routed based on the system routing table");
     }
+
+    if ((bool)(o->initial_cf_bloom_config.inet_tiers)
+        +(bool)(o->initial_cf_bloom_config.inet6_tiers) == 1)
+    {
+        msg(M_FATAL, "connect-freq-initial-bloom-limit must be provided for "
+            "both inet and inet6");
+    }
+    check_filter_tier_under_global_limit(o->initial_cf_bloom_config.inet_tiers,
+                                         o->cf_initial_max, "inet");
+    check_filter_tier_under_global_limit(o->initial_cf_bloom_config.inet6_tiers,
+                                         o->cf_initial_max, "inet6");
+
 }
 
 
@@ -7562,6 +7590,71 @@
         options->cf_initial_max = cf_max;
         options->cf_initial_per = cf_per;
     }
+    else if (streq(p[0], "connect-freq-initial-bloom-size") && p[1] && !p[3])
+    {
+        VERIFY_PERMISSION(OPT_P_GENERAL);
+
+        int cf_bloom_size = atoi(p[1]);
+
+        if (cf_bloom_size <= 0)
+        {
+            msg(msglevel, "--connect-freq-initial-bloom-size size must be > 0");
+            goto err;
+        }
+        options->initial_cf_bloom_config.size = cf_bloom_size;
+        if (p[2])
+        {
+            int num_hash = atoi(p[1]);
+            if (num_hash <= 0)
+            {
+                msg(msglevel, "--connect-freq-initial-bloom-size number of hash "
+                    "functions must be > 0");
+                goto err;
+            }
+            options->initial_cf_bloom_config.num_hashes = num_hash;
+        }
+    }
+    else if (streq(p[0], "connect-freq-initial-bloom-limit") && p[1] && p[2]
+             && p[3] && !p[4])
+    {
+        int netmask = atoi(p[2]);
+        int limit = atoi(p[3]);
+
+        if (limit <= 0)
+        {
+            msg(msglevel, "Limit parameter to %s must be > 0", p[0]);
+            goto err;
+        }
+
+        if (streq(p[1], "inet"))
+        {
+            if (netmask < 0 || netmask > 32)
+            {
+                msg(msglevel, "Netmask parameter (%s) for IPv4 for %s must be "
+                    "between 0 and 32", p[2], p[0]);
+                goto err;
+            }
+            reflect_add_filter_tier(&options->initial_cf_bloom_config,
+                                    &options->gc, false, netmask, limit);
+        }
+        else if (streq(p[1], "inet6"))
+        {
+            if (netmask < 0 || netmask > 128)
+            {
+                msg(msglevel, "Netmask parameter (%s) for IPv6 for %s must be "
+                    "between 0 and 128", p[2], p[0]);
+                goto err;
+            }
+            reflect_add_filter_tier(&options->initial_cf_bloom_config,
+                                    &options->gc, true, netmask, limit);
+        }
+        else
+        {
+            msg(msglevel, "Unknown parameter %s for %s. Must be inet or inet6",
+                p[1], p[0]);
+            goto err;
+        }
+    }
     else if (streq(p[0], "max-clients") && p[1] && !p[2])
     {
         int max_clients;
diff --git a/src/openvpn/options.h b/src/openvpn/options.h
index f608cb8..8448071 100644
--- a/src/openvpn/options.h
+++ b/src/openvpn/options.h
@@ -43,6 +43,7 @@
 #include "clinat.h"
 #include "crypto_backend.h"
 #include "dns.h"
+#include "reflect_filter.h"
 
 
 /*
@@ -515,6 +516,8 @@
     int cf_initial_max;
     int cf_initial_per;
 
+    struct bloom_filter_conf initial_cf_bloom_config;
+
     int max_clients;
     int max_routes_per_client;
     int stale_routes_check_interval;
diff --git a/src/openvpn/reflect_filter.c b/src/openvpn/reflect_filter.c
index 538bbbc..75445c0 100644
--- a/src/openvpn/reflect_filter.c
+++ b/src/openvpn/reflect_filter.c
@@ -38,8 +38,7 @@
 #include "crypto.h"
 #include "reflect_filter.h"
 
-
-bool
+static bool
 reflect_filter_rate_limit_check(struct initial_packet_rate_limit *irl)
 {
     if (now > irl->last_period_reset + irl->period_length)
@@ -72,34 +71,384 @@
     return !over_limit;
 }
 
-void
-reflect_filter_rate_limit_decrease(struct initial_packet_rate_limit *irl)
+static void
+reset_filter_tier(struct initial_packet_rate_limit *irl,
+                  struct filter_tier *tier, const char *prefix)
 {
+    for (; tier != NULL; tier = tier->next)
+    {
+        if (tier->dropped > 0)
+        {
+            msg(D_TLS_DEBUG_LOW, "Dropped %zu initial handshake packets due to "
+                "--connect-freq-initial-bloom-limit %s %d %d",
+                tier->dropped, prefix, tier->netmask, tier->limit);
+            tier->dropped = 0;
+        }
+    }
+}
+
+static void
+bloom_filter_check_reset(struct initial_packet_rate_limit *irl)
+{
+    if (now > irl->last_period_reset + irl->period_length)
+    {
+        reset_filter_tier(irl, irl->bloom_conf.inet_tiers, "inet");
+        reset_filter_tier(irl, irl->bloom_conf.inet6_tiers, "inet6");
+
+        bloom_clear(irl->bf);
+    }
+}
+
+
+/**
+ * structure used as lookup key for the bloom structure. We used the
+ * netmask as part of the structure to avoid the look for the first
+ * IP of a subnet and the subnet to be same key.
+ */
+struct bloom_filter_key {
+    union {
+        struct in_addr in;
+        struct in6_addr in6;
+    };
+    int netmask;
+    /* we keep the count in the key instead of in the bloom filter table as
+     * can then keep the counter in the bloom filter itself small (2 bits)
+     * and bloom filter usage is the same for 20000 request from the same IP
+     * (20k entries with different count but same IP) and from 20000 random ips
+     * (20k entries with count 1 but different IP) */
+    int count;
+};
+
+static inline struct bloom_filter_key
+filter_mask_inet6(struct openvpn_sockaddr *addr, int netmask)
+{
+    struct bloom_filter_key ret = { 0 };
+    ret.in6 = addr->addr.in6.sin6_addr;
+
+    int bits_to_clear = 128 - netmask;
+    int bytes_to_clear =  bits_to_clear /8;
+    bits_to_clear = bits_to_clear % 8;
+
+    memset(&ret.in6.s6_addr[15 - bytes_to_clear], 0x00, bytes_to_clear);
+
+    ret.in6.s6_addr[15 - bytes_to_clear - 1] &= (0xff << bits_to_clear);
+
+    ret.netmask = netmask;
+
+    return ret;
+}
+
+static inline struct bloom_filter_key
+filter_mask_inet(struct openvpn_sockaddr *addr, int netmask)
+{
+    struct bloom_filter_key ret = { 0 };
+    ret.in = addr->addr.in4.sin_addr;
+    ret.in.s_addr &= htonl(0xffffffff << (32 - netmask));
+    ret.netmask = netmask;
+    return ret;
+}
+
+/* We use one function and an action argument to avoid repeating
+ * the code to iterate to the tiers and the creating the lookup
+ * keys */
+enum bloom_filter_action
+{
+    REFLECT_CHECK,
+    REFLECT_INCREASE,
+    REFLECT_DECREASE,
+};
+
+static int
+reflect_lookup_bf_key(struct bloom_filter *bf, struct bloom_filter_key *key, int limit)
+{
+    /* we do a lookup for 1,2,3, and 4 and the limit first
+     * and after that do regular binary search. This is meant to optimise
+     * the common case where just a small number of requests are coming from
+     * each IP */
+    key->count = limit;
+    if (bloom_test(bf, (const uint8_t *) key, sizeof(struct bloom_filter_key)) > 0)
+    {
+        return limit;
+    }
+
+    key->count = 4;
+    if (bloom_test(bf, (const uint8_t *) key, sizeof(struct bloom_filter_key)) == 0)
+    {
+        /* The value for 4 has not been found, so the real value might be 1, 2, or 3. */
+        for (int i = 3; i > 0; i--)
+        {
+            key->count = i;
+            if (bloom_test(bf, (const uint8_t *) key, sizeof(struct bloom_filter_key)) > 0)
+            {
+                return i;
+            }
+        }
+
+        /* 4 was no in the map and 1-3 are also not there, so assume the key is not in the map */
+        return 0;
+    }
+
+    int low = 3;
+    int high = limit;
+
+    while (low < high)
+    {
+
+        key->count = (high + low + 1)/2;
+        bloom_counter_t count = bloom_test(bf, (const uint8_t *) key, sizeof(struct bloom_filter_key));
+        if (count > 0)
+        {
+            low = key->count;
+        }
+        else
+        {
+            high = key->count - 1;
+        }
+    }
+
+    if (low == 4)
+    {
+        /* we reached the lower end of our binary search have not found the
+         * key and we know that 4 is in the map */
+        return 4;
+    }
+    else
+    {
+        return low;
+    }
+}
+
+/**
+ * Convert a mapped IPv6 mapped IPv4 address (::ffff:0:0/96) to an
+ * equivalent IPv4 adress.
+ *
+ * @note: This function only converts the IP itself and ignores other
+ * parts of the \c from structure like port or protocol.
+ */
+static struct openvpn_sockaddr
+convert_mapped_inet_sockaddr(struct openvpn_sockaddr *from)
+{
+    struct openvpn_sockaddr from_mapped = { 0 };
+    /* we ignore the fields like port that the key in the bloom filter
+     * ignores too. This makes this function non-generic */
+
+    from_mapped.addr.in4.sin_family = AF_INET;
+
+    memcpy(&from_mapped.addr.in4.sin_addr.s_addr,
+           &from->addr.in6.sin6_addr.s6_addr[12],
+           sizeof(from_mapped.addr.in4.sin_addr.s_addr));
+
+    return from_mapped;
+}
+
+static bool
+bloom_filter_action(struct initial_packet_rate_limit *irl,
+                    struct openvpn_sockaddr *from,
+                    enum bloom_filter_action action)
+{
+    bool found = false;
+    struct filter_tier *tier = NULL;
+
+    struct openvpn_sockaddr from_mapped;
+
+    if (from->addr.sa.sa_family == AF_INET6
+        && IN6_IS_ADDR_V4MAPPED(&from->addr.in6.sin6_addr))
+    {
+        from_mapped = convert_mapped_inet_sockaddr(from);
+        from = &from_mapped;
+    }
+
+    if (from->addr.sa.sa_family == AF_INET)
+    {
+        tier = irl->bloom_conf.inet_tiers;
+
+    }
+    else if (from->addr.sa.sa_family == AF_INET6)
+    {
+        tier = irl->bloom_conf.inet6_tiers;
+    }
+
+    while (tier)
+    {
+        struct filter_tier *next_tier = tier->next;
+        struct bloom_filter_key key;
+
+        if (from->addr.sa.sa_family == AF_INET6)
+        {
+            key = filter_mask_inet6(from, tier->netmask);
+        }
+        else
+        {
+            key = filter_mask_inet(from, tier->netmask);
+        }
+
+        /* fetch the current count of the key in the bloom filter */
+        int result = reflect_lookup_bf_key(irl->bf, &key, tier->limit);
+        struct gc_arena gc = gc_new();
+        gc_free(&gc);
+
+        switch (action)
+        {
+
+            case REFLECT_CHECK:
+                if (result >= tier->limit)
+                {
+                    found = true;
+                    tier->dropped++;
+                    if (tier->dropped == 1)
+                    {
+                        msg(M_WARN, "Note: --connect-freq-initial-bloom-limit "
+                            "limit for netmask /%d exceeded. Expect additional "
+                            "initial packet drops for the next %d seconds",
+                            tier->netmask,
+                            (int)(irl->last_period_reset + irl->period_length - now));
+                    }
+                }
+                break;
+
+            case REFLECT_INCREASE:
+                ASSERT(result < tier->limit);
+                key.count = result + 1;
+                bloom_add(irl->bf, (const uint8_t *) &key, sizeof(key));
+                break;
+
+            case REFLECT_DECREASE:
+                key.count = result - 1;
+                bloom_remove(irl->bf, (const uint8_t *) &key, sizeof(key));
+                break;
+        }
+
+        tier = next_tier;
+
+    }
+    if (!found && action == REFLECT_CHECK)
+    {
+        /* We only want to increase the counters if the IP is not already
+         * in the set. */
+        bloom_filter_action(irl, from, REFLECT_INCREASE);
+    }
+    return found;
+}
+
+static bool
+bloom_filter_check(struct initial_packet_rate_limit *irl,
+                   struct openvpn_sockaddr *from)
+{
+    if (now > irl->last_period_reset + irl->period_length)
+    {
+
+        bloom_filter_check_reset(irl);
+        bloom_clear(irl->bf);
+    }
+
+    return bloom_filter_action(irl, from, REFLECT_CHECK);
+}
+
+
+bool
+reflect_filter_check(struct initial_packet_rate_limit *irl,
+                     struct openvpn_sockaddr *from)
+{
+    /* We are doing the bloom filter check first so packets that are already
+     * rejected by the bloom filter do not count against the limit of the
+     * simple rate limiter */
+    if (irl->bf && bloom_filter_check(irl, from))
+    {
+        return false;
+    }
+
+    if (!reflect_filter_rate_limit_check(irl))
+    {
+        return false;
+    }
+
+    return true;
+}
+
+
+void
+reflect_filter_rate_limit_decrease(struct initial_packet_rate_limit *irl, struct openvpn_sockaddr *from)
+{
+    if (irl->bf && bloom_filter_action(irl, from, REFLECT_CHECK))
+    {
+        /* Only remove if it is actually present. This might be a packet
+         * coming from an early period or be relayed */
+        bloom_filter_action(irl, from, REFLECT_DECREASE);
+    }
+
     if (irl->curr_period_counter > 0)
     {
         irl->curr_period_counter--;
     }
 }
 
+void
+reflect_add_filter_tier(struct bloom_filter_conf *bfconf, struct gc_arena *gc,
+                        bool ipv6, int netmask, int limit)
+{
+    struct filter_tier *ftnew = gc_malloc(sizeof(struct filter_tier), true, gc);
+
+    ftnew->netmask = netmask;
+    ftnew->limit = limit;
+
+    if (ipv6)
+    {
+        ftnew->next = bfconf->inet6_tiers;
+        bfconf->inet6_tiers = ftnew;
+    }
+    else
+    {
+        ftnew->next = bfconf->inet_tiers;
+        bfconf->inet_tiers = ftnew;
+    }
+}
+
+void
+init_bloom_filter(struct initial_packet_rate_limit *irl)
+{
+    if (!irl->bloom_conf.size)
+    {
+        /* the default allocates 2MB for bloom filter entries */
+        irl->bloom_conf.size = 1024ul * 1024 * 8;
+    }
+    if (!irl->bloom_conf.num_hashes)
+    {
+        irl->bloom_conf.num_hashes = 7;
+    }
+
+    irl->bf = bloom_create(irl->bloom_conf.size, irl->bloom_conf.num_hashes,
+                           &irl->gc);
+    bloom_clear(irl->bf);
+}
 
 struct initial_packet_rate_limit *
-initial_rate_limit_init(int max_per_period, int period_length)
+initial_rate_limit_init(int max_per_period, int period_length,
+                        struct bloom_filter_conf *bconf)
 {
-    struct initial_packet_rate_limit *irl;
 
+    struct initial_packet_rate_limit *irl = NULL;
+    ALLOC_OBJ_CLEAR(irl, struct initial_packet_rate_limit);
 
-    ALLOC_OBJ(irl, struct initial_packet_rate_limit);
+    irl->gc = gc_new();
 
     irl->max_per_period = max_per_period;
     irl->period_length = period_length;
     irl->curr_period_counter = 0;
     irl->last_period_reset = 0;
 
+    if (bconf)
+    {
+        irl->bloom_conf = *bconf;
+        init_bloom_filter(irl);
+    }
+
     return irl;
 }
 
 void
 initial_rate_limit_free(struct initial_packet_rate_limit *irl)
 {
+    gc_free(&irl->gc);
     free(irl);
+    irl = NULL;
 }
diff --git a/src/openvpn/reflect_filter.h b/src/openvpn/reflect_filter.h
index 12eb0a1..c639888 100644
--- a/src/openvpn/reflect_filter.h
+++ b/src/openvpn/reflect_filter.h
@@ -24,6 +24,28 @@
 #define REFLECT_FILTER_H
 
 #include <limits.h>
+#include "socket.h"
+#include "bloom.h"
+
+struct filter_tier {
+    struct filter_tier *next;
+
+    int limit;
+    int netmask;
+
+    /** The number of packets we dropped since we went over this limit */
+    size_t dropped;
+};
+
+struct bloom_filter_conf {
+    struct filter_tier *inet_tiers;
+    struct filter_tier *inet6_tiers;
+
+    /* Configuration of the bloom filter */
+    size_t num_hashes;
+    size_t size;
+};
+
 
 /** struct that handles all the rate limiting logic for initial
  * responses */
@@ -35,7 +57,7 @@
     int period_length;
 
     /** Number of packets in the current period. We use int64_t here
-     * to avoid any potiential issues with overflow */
+     * to avoid any potential issues with overflow */
     int64_t curr_period_counter;
 
     /* Last time we reset our timer */
@@ -44,15 +66,28 @@
     /* we want to warn once per period that packets are being started to
      * be dropped */
     bool warning_displayed;
+
+    struct bloom_filter_conf bloom_conf;
+    struct bloom_filter *bf;
+
+    /* gc_arena used for the various allocations by this struct */
+    struct gc_arena gc;
 };
 
+/**
+ * Adds a bloom filter tier to the bloom filter config.
+ */
+void
+reflect_add_filter_tier(struct bloom_filter_conf *bfconf, struct gc_arena *gc,
+                        bool ipv6, int netmask, int limit);
 
 /**
  * checks if the connection is still allowed to connect under the rate
  * limit. This also increases the internal counter at the same time
  */
 bool
-reflect_filter_rate_limit_check(struct initial_packet_rate_limit *irl);
+reflect_filter_check(struct initial_packet_rate_limit *irl,
+                     struct openvpn_sockaddr *from);
 
 /**
  * decreases the counter of initial packets seen, so connections that
@@ -60,16 +95,27 @@
  * the counter of initial connection attempts
  */
 void
-reflect_filter_rate_limit_decrease(struct initial_packet_rate_limit *irl);
+reflect_filter_rate_limit_decrease(struct initial_packet_rate_limit *irl,
+                                   struct openvpn_sockaddr *from);
 
 /**
  * allocate and initialize the initial-packet rate limiter structure
+ *
+ * Note: this function does not copy bconf's contents.
  */
 struct initial_packet_rate_limit *
-initial_rate_limit_init(int max_per_period, int period_length);
+initial_rate_limit_init(int max_per_period, int period_length,
+                        struct bloom_filter_conf *bconf);
 
 /**
  * free the initial-packet rate limiter structure
  */
 void initial_rate_limit_free(struct initial_packet_rate_limit *irl);
+
+/**
+ * Initialises the bloom filter with the configuration values of
+ * irl->bloom_conf
+ */
+void
+init_bloom_filter(struct initial_packet_rate_limit *irl);
 #endif /* ifndef REFLECT_FILTER_H */
diff --git a/tests/unit_tests/openvpn/Makefile.am b/tests/unit_tests/openvpn/Makefile.am
index a4e6235..cd1c378 100644
--- a/tests/unit_tests/openvpn/Makefile.am
+++ b/tests/unit_tests/openvpn/Makefile.am
@@ -11,7 +11,7 @@
 endif
 
 test_binaries += crypto_testdriver packet_id_testdriver auth_token_testdriver ncp_testdriver misc_testdriver \
-	pkt_testdriver ssl_testdriver user_pass_testdriver
+	pkt_testdriver ssl_testdriver user_pass_testdriver reflect_testdriver
 
 if HAVE_LD_WRAP_SUPPORT
 if !WIN32
@@ -144,6 +144,22 @@
 	$(top_srcdir)/src/openvpn/win32-util.c \
 	$(top_srcdir)/src/openvpn/tls_crypt.c
 
+reflect_testdriver_CFLAGS  = @TEST_CFLAGS@ \
+	-I$(top_srcdir)/include -I$(top_srcdir)/src/compat -I$(top_srcdir)/src/openvpn
+reflect_testdriver_LDFLAGS = @TEST_LDFLAGS@
+reflect_testdriver_SOURCES = test_reflect.c mock_msg.c mock_msg.h \
+	$(top_srcdir)/src/openvpn/reflect_filter.c \
+	$(top_srcdir)/src/openvpn/bloom.c \
+	$(top_srcdir)/src/openvpn/buffer.c \
+	$(top_srcdir)/src/openvpn/crypto.c \
+	$(top_srcdir)/src/openvpn/crypto_mbedtls.c \
+	$(top_srcdir)/src/openvpn/crypto_openssl.c \
+	$(top_srcdir)/src/openvpn/otime.c \
+	$(top_srcdir)/src/openvpn/packet_id.c \
+	$(top_srcdir)/src/openvpn/platform.c \
+	$(top_srcdir)/src/openvpn/siphash_reference.c \
+	$(top_srcdir)/src/openvpn/win32-util.c
+
 if !WIN32
 tls_crypt_testdriver_CFLAGS  = \
 	-I$(top_srcdir)/include -I$(top_srcdir)/src/compat -I$(top_srcdir)/src/openvpn \
diff --git a/tests/unit_tests/openvpn/test_reflect.c b/tests/unit_tests/openvpn/test_reflect.c
new file mode 100644
index 0000000..5158631
--- /dev/null
+++ b/tests/unit_tests/openvpn/test_reflect.c
@@ -0,0 +1,327 @@
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2016-2021 Fox Crypto B.V. <openvpn@foxcrypto.com>
+ *
+ *  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 (see the file COPYING included with this
+ *  distribution); if not, write to the Free Software Foundation, Inc.,
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#elif defined(_MSC_VER)
+#include "config-msvc.h"
+#endif
+
+#include "syshead.h"
+
+#include "bloom.h"
+#include "buffer.h"
+#include "reflect_filter.h"
+#include "test_common.h"
+
+#include <setjmp.h>
+#include <stdarg.h>
+#include <cmocka.h>
+
+#include <stdio.h>
+
+
+
+static void
+test_bloom(void **state)
+{
+    static const int present_mod = 77;
+
+    struct gc_arena gc = gc_new();
+    /* Use a bloom filter with 1M entries (256kB) for the unit test */
+    struct bloom_filter *bf = bloom_create(1024ul*1024, 8, &gc);
+
+    for (int32_t i = 0; i < 20000; i += present_mod)
+    {
+        bloom_add(bf, (const uint8_t *) &i, sizeof(i));
+    }
+
+    /* all these should be positive, for the small unit test we do not expect
+     * false positive */
+    for (int32_t i = 0; i < 20000; i++)
+    {
+        int present = (i % present_mod == 0 ) ? 1 : 0;
+        /* cast to bool to only get 1 and 0 for present/not present and not */
+        assert_int_equal((bool) bloom_test(bf, (const uint8_t *) &i, sizeof(i)), present);
+    }
+
+    bloom_free(bf);
+    gc_free(&gc);
+}
+
+static void
+test_reflect_ddos(void **state)
+{
+    /* This tests if the bloom filter implementation does actually work with
+     * the goal of dropping packets to a reflected /24 while still allowing
+     * other clients */
+
+    /* Disable the normal fallback that puts a hard cap on the reflection filter */
+    struct initial_packet_rate_limit *irl = initial_rate_limit_init(INT_MAX, 300, NULL);
+
+    reflect_add_filter_tier(&irl->bloom_conf, &irl->gc, false, 24, 5);
+    reflect_add_filter_tier(&irl->bloom_conf, &irl->gc, false, 8, 20);
+    init_bloom_filter(irl);
+
+    int num_legimate_reject = 0;
+    int num_legimate_accepted = 0;
+
+    int num_ddos_rejected = 0;
+    int num_ddos_accepted = 0;
+
+
+    /* /24 net addresses in host byte order */
+    in_addr_t net_attack[4];
+    for (int i = 0; i < 3; i++)
+    {
+        net_attack[i] = random() % 0xff;
+    }
+
+    /* The 4th network is close enough to the 3rd to fall in the same /16 */
+    /* XOR the 3rd byte to achieve this */
+    net_attack[3] = net_attack[2] ^ 00002300;
+
+    /* Assume 200000 packets, including roughly 200 legitimate packets,
+     * the unit test works also with 20 millions packet but takes too long*/
+    static const int total_packets = 200 * 1000;
+
+    for (int i = 0; i < total_packets; i++)
+    {
+        struct openvpn_sockaddr from = { 0 };
+        from.addr.in4.sin_family = AF_INET;
+        from.addr.in4.sin_addr.s_addr = random();
+        from.addr.in4.sin_port = random();
+
+        if (i % 1023 == 0)
+        {
+            /* roughly 200 legitimate clients with random addresses */
+            from.addr.in4.sin_addr.s_addr = random();
+
+            bool allowed = reflect_filter_check(irl, &from);
+            if (allowed)
+            {
+                num_legimate_accepted++;
+            }
+            else
+            {
+                num_legimate_reject++;
+            }
+
+        }
+        else
+        {
+            /* We attack the 4 networks at random */
+            from.addr.in4.sin_addr.s_addr  = net_attack[random() % 4] + (random() % 256);
+
+            bool allowed  = reflect_filter_check(irl, &from);
+            if (allowed)
+            {
+                num_ddos_accepted++;
+            }
+            else
+            {
+                num_ddos_rejected++;
+            }
+        }
+    }
+
+    assert_int_equal(num_legimate_reject + num_legimate_accepted + num_ddos_accepted + num_ddos_rejected, total_packets);
+
+    /* We assume that most legitimate made it through but a few were unfortunate to be in an attacked network */
+    assert_in_range(num_legimate_reject, 0, 10);
+
+    /* We disabled total number of packets, so we expect all /8 to have their
+     * 20 packets, which is 5120. */
+    assert_in_range(num_ddos_accepted, 0, 256 * 20);
+
+    initial_rate_limit_free(irl);
+}
+
+
+static void
+test_bloom_minimal(void **state)
+{
+    struct gc_arena gc = gc_new();
+    struct bloom_filter *bf = bloom_create(2048, 3, &gc);
+
+    int item = 0xbabe;
+
+    bloom_add(bf, (const uint8_t *) &item, sizeof(item));
+    assert_int_equal(bloom_test(bf, (const uint8_t *) &item, sizeof(item)), 1);
+
+    item = 0xf00f;
+    assert_int_equal(bloom_test(bf, (const uint8_t *) &item, sizeof(item)), 0);
+
+    bloom_free(bf);
+    gc_free(&gc);
+}
+
+static void
+test_reflect_reflect_bloom_simple(void **state)
+{
+    struct initial_packet_rate_limit *irl = initial_rate_limit_init(INT_MAX, 300, NULL);
+
+    reflect_add_filter_tier(&irl->bloom_conf, &irl->gc, true, 32, 50);
+    reflect_add_filter_tier(&irl->bloom_conf, &irl->gc, true, 56, 200);
+    init_bloom_filter(irl);
+
+    struct openvpn_sockaddr from = { 0 };
+    from.addr.in6.sin6_family = AF_INET6;
+    from.addr.in6.sin6_port = random();
+    from.addr.in6.sin6_addr.s6_addr[15] = 1;  /* ::1 */
+
+    /* There are 50 attempts that should work until one fails */
+    for (int i = 0; i < 50; i++)
+    {
+        assert_true(reflect_filter_check(irl, &from));
+    }
+
+    /* 2002::1 */
+    struct openvpn_sockaddr from2 = from;
+    from2.addr.in6.sin6_addr.s6_addr[0] = 0x7;
+    from2.addr.in6.sin6_addr.s6_addr[1] = 0x7;
+
+    assert_true(reflect_filter_check(irl, &from2));
+
+    /* Any more attempts from ::1 should fail */
+    assert_false(reflect_filter_check(irl, &from));
+
+    initial_rate_limit_free(irl);
+}
+
+static void
+test_reflect_bloom_netmask_masking(void **state)
+{
+    struct initial_packet_rate_limit *irl = initial_rate_limit_init(INT_MAX, 300, NULL);
+
+    reflect_add_filter_tier(&irl->bloom_conf, &irl->gc, true, 45, 10);
+    init_bloom_filter(irl);
+
+    struct openvpn_sockaddr from = { 0 };
+    from.addr.in6.sin6_family = AF_INET6;
+    from.addr.in6.sin6_port = random();
+    for (int i = 0; i < 15; i++)
+    {
+        from.addr.in6.sin6_addr.s6_addr[i] = random();
+    }
+
+    for (int i = 0; i < 10; i++)
+    {
+        /* /45 means that if we leave the leave first 8 bytes and 5 bits
+         * untouched, it is still the same subnet  */
+        for (int j = 8; j<15; j++)
+        {
+            from.addr.in6.sin6_addr.s6_addr[j] = random();
+            from.addr.in6.sin6_addr.s6_addr[7] ^= random() & 0x7;
+        }
+        assert_true(reflect_filter_check(irl, &from));
+
+    }
+
+    /* testing the last IP again should give us a negative result */
+    assert_false(reflect_filter_check(irl, &from));
+
+    initial_rate_limit_free(irl);
+}
+
+
+static void
+test_reflect_reflect_bloom_mapped(void **state)
+{
+    struct initial_packet_rate_limit *irl = initial_rate_limit_init(INT_MAX, 300, NULL);
+
+    reflect_add_filter_tier(&irl->bloom_conf, &irl->gc, false, 24, 50);
+    reflect_add_filter_tier(&irl->bloom_conf, &irl->gc, false, 8, 80);
+    init_bloom_filter(irl);
+
+    /* Our OpenVPN server will not receive IPv4 as well as IPv4 mapped
+     * addresses in the same process but for the unit test it is convient
+     * to see if they actually mapped to the same entries */
+
+    struct openvpn_sockaddr mapped_v4 =  { 0 };
+    mapped_v4.addr.in6.sin6_family = AF_INET6;
+    mapped_v4.addr.in6.sin6_port = random();
+    /* ::ffff:192.168.0.99 */
+    mapped_v4.addr.in6.sin6_addr.s6_addr[10] = 0xff;
+    mapped_v4.addr.in6.sin6_addr.s6_addr[11] = 0xff;
+    mapped_v4.addr.in6.sin6_addr.s6_addr[12] = 192;
+    mapped_v4.addr.in6.sin6_addr.s6_addr[13] = 168;
+    mapped_v4.addr.in6.sin6_addr.s6_addr[14] = 0;
+    mapped_v4.addr.in6.sin6_addr.s6_addr[15] = 99;
+
+    assert_true(IN6_IS_ADDR_V4MAPPED(&mapped_v4.addr.in6.sin6_addr));
+
+    /* Not the same address but in the same /8 */
+    struct openvpn_sockaddr v4addr = {0 };
+    v4addr.addr.in4.sin_family = AF_INET;
+    v4addr.addr.in4.sin_port = random();
+    /* 192.168.123.244 */
+    v4addr.addr.in4.sin_addr.s_addr = htonl(0xc0a87bf4);
+
+    /* check that we run into the 50 limit with our mapped address */
+    for (int i = 0; i < 50; i++)
+    {
+        assert_true(reflect_filter_check(irl, &mapped_v4));
+    }
+    assert_false(reflect_filter_check(irl, &mapped_v4));
+
+
+    /* Check that the non-mapped IPv4 address uses the same /8 subnet limit */
+    for (int i = 0; i < 30; i++)
+    {
+        assert_true(reflect_filter_check(irl, &v4addr));
+    }
+    assert_false(reflect_filter_check(irl, &v4addr));
+
+    initial_rate_limit_free(irl);
+}
+
+
+static void
+test_bloom_access_functions(void **state)
+{
+    static_assert(BLOOM_FILTER_BITS_COUNT == 2, "unit test not in sync");
+    static_assert(BLOOM_FILTER_BITS_MASK == 0x3, "unit test not in sync");
+}
+
+
+int
+main(void)
+{
+    openvpn_unit_test_setup();
+    const struct CMUnitTest tests[] = {
+        cmocka_unit_test(test_bloom_access_functions),
+        cmocka_unit_test(test_bloom),
+        cmocka_unit_test(test_bloom_minimal),
+        cmocka_unit_test(test_reflect_reflect_bloom_simple),
+        cmocka_unit_test(test_reflect_reflect_bloom_mapped),
+        cmocka_unit_test(test_reflect_bloom_netmask_masking),
+        cmocka_unit_test(test_reflect_ddos),
+    };
+
+
+    int ret = cmocka_run_group_tests_name("crypto tests", tests, NULL, NULL);
+
+    return ret;
+}
