Commit 4fa5707c authored by Marek Vavrusa's avatar Marek Vavrusa

Replaced ACL data structure with prefix-sorted linked lists.

This is O(N) on both insert/search, but since the ACL lists are
several items long (usually), it is not an issue.
The lists are sorted from the longest prefix to the shortest, so
the first match is guaranteed to be longest prefix match.
parent f4226c28
......@@ -13,5 +13,4 @@ current version of Knot.
Known bugs
==========
* ACL may not always find the best match so it may behave counter-intuitively.
* Rarely, incremental transfer fails to reschedule
......@@ -20,23 +20,25 @@
#include <sys/types.h>
#include <sys/socket.h>
#include <limits.h>
#include <stdbool.h>
#include "common/errcode.h"
#include "common/acl.h"
#include "libknot/util/endian.h"
static inline uint32_t ipv4_chunk(sockaddr_t *a)
static inline uint32_t ipv4_chunk(const sockaddr_t *a)
{
/* Stored as big end first. */
return a->addr4.sin_addr.s_addr;
}
static inline uint32_t ipv6_chunk(sockaddr_t *a, uint8_t idx)
static inline uint32_t ipv6_chunk(const sockaddr_t *a, uint8_t idx)
{
/* Is byte array, 4x 32bit value. */
return ((uint32_t *)&a->addr6.sin6_addr)[idx];
}
static inline uint32_t ip_chunk(sockaddr_t *a, uint8_t idx)
static inline uint32_t ip_chunk(const sockaddr_t *a, uint8_t idx)
{
if (sockaddr_family(a) == AF_INET)
return ipv4_chunk(a);
......@@ -45,10 +47,11 @@ static inline uint32_t ip_chunk(sockaddr_t *a, uint8_t idx)
}
/*! \brief Compare chunks using given mask. */
static int cmp_chunk(sockaddr_t *a, sockaddr_t *b, uint8_t idx, uint32_t mask)
static int cmp_chunk(const sockaddr_t *a1, const sockaddr_t *a2,
uint8_t idx, uint32_t mask)
{
const uint32_t c1 = ip_chunk(a, idx) & mask;
const uint32_t c2 = ip_chunk(b, idx) & mask;
const uint32_t c1 = ip_chunk(a1, idx) & mask;
const uint32_t c2 = ip_chunk(a2, idx) & mask;
if (c1 > c2)
return 1;
......@@ -77,11 +80,9 @@ static uint32_t acl_fill_mask32(short nbits)
return htonl(r);
}
static int acl_compare(void *k1, void *k2)
static int acl_compare(const sockaddr_t *a1, const sockaddr_t *a2)
{
int ret = 0;
sockaddr_t* a1 = (sockaddr_t *)k1;
sockaddr_t* a2 = (sockaddr_t *)k2;
uint32_t mask = 0xffffffff;
short mask_bits = a1->prefix;
const short chunk_bits = sizeof(mask) * CHAR_BIT;
......@@ -114,128 +115,103 @@ static int acl_compare(void *k1, void *k2)
return ret;
}
acl_t *acl_new(acl_rule_t default_rule, const char *name)
acl_t *acl_new()
{
/* Trailing '\0' for NULL name. */
size_t name_len = 1;
if (name) {
name_len += strlen(name);
} else {
name = "";
}
/* Allocate memory for ACL. */
acl_t* acl = malloc(sizeof(acl_t) + name_len);
if (!acl) {
return 0;
}
/* Initialize skip list. */
acl->rules = skip_create_list(acl_compare);
if (!acl->rules) {
free(acl);
return 0;
}
/* Initialize skip list for rules with TSIG. */
/*! \todo This needs a better structure to make
* nodes with TSIG preferred, but for now
* it will do to sort nodes into two lists.
* (issue #1675)
*/
acl->rules_pref = skip_create_list(acl_compare);
if (!acl->rules_pref) {
skip_destroy_list(&acl->rules, 0, free);
free(acl);
return 0;
acl_t *acl = malloc(sizeof(acl_t));
if (acl == NULL) {
return NULL;
}
/* Initialize. */
memcpy(&acl->name, name, name_len);
acl->default_rule = default_rule;
memset(acl, 0, sizeof(acl_t));
init_list(acl);
return acl;
}
void acl_delete(acl_t **acl)
{
if ((acl == NULL) || (*acl == NULL)) {
if (acl == NULL || *acl == NULL) {
return;
}
/* Truncate rules. */
skip_destroy_list(&(*acl)->rules, 0, free);
skip_destroy_list(&(*acl)->rules_pref, 0, free);
acl_truncate(*acl);
/* Free ACL. */
free(*acl);
*acl = 0;
}
int acl_create(acl_t *acl, const sockaddr_t* addr, acl_rule_t rule, void *val,
unsigned flags)
int acl_insert(acl_t *acl, const sockaddr_t *addr, void *val)
{
if (!acl || !addr) {
return ACL_ERROR;
if (acl == NULL || addr == NULL) {
return KNOT_EINVAL;
}
/* Insert into skip list. */
acl_key_t *key = malloc(sizeof(acl_key_t));
/* Create new match. */
acl_match_t *key = malloc(sizeof(acl_match_t));
if (key == NULL) {
return ACL_ERROR;
return KNOT_ENOMEM;
}
memcpy(&key->addr, addr, sizeof(sockaddr_t));
key->rule = rule;
key->val = val;
if (flags & ACL_PREFER) {
skip_insert(acl->rules_pref, &key->addr, key, 0);
/* Sort by prefix length.
* This way the longest prefix match always goes first.
*/
if (EMPTY_LIST(*acl)) {
add_head(acl, &key->n);
} else {
skip_insert(acl->rules, &key->addr, key, 0);
bool inserted = false;
acl_match_t *cur = NULL, *prev = NULL;
WALK_LIST(cur, *acl) {
/* Next node prefix is equal/shorter than current key.
* This means we need to insert before the next node.
*/
if (cur->addr.prefix < addr->prefix) {
if (prev == NULL) { /* First node. */
add_head(acl, &key->n);
} else {
insert_node(&key->n, &prev->n);
}
inserted = true;
break;
}
prev = cur;
}
/* Didn't find any better fit, insert at the end. */
if (!inserted) {
add_tail(acl, &key->n);
}
}
return ACL_ACCEPT;
return KNOT_EOK;
}
int acl_match(acl_t *acl, const sockaddr_t* addr, acl_key_t **key)
acl_match_t* acl_find(acl_t *acl, const sockaddr_t *addr)
{
if (!acl || !addr) {
return ACL_ERROR;
if (acl == NULL || addr == NULL) {
return NULL;
}
acl_key_t *found = skip_find(acl->rules_pref, (void*)addr);
if (found == NULL) {
found = skip_find(acl->rules, (void*)addr);
}
/* Set stored value if exists. */
if (key != NULL) {
*key = found;
}
/* Return appropriate rule. */
if (found == NULL) {
return acl->default_rule;
acl_match_t *cur = NULL;
WALK_LIST(cur, *acl) {
/* Since the list is sorted by prefix length, the first match
* is guaranteed to be longest prefix match (most precise).
*/
if (acl_compare(&cur->addr, addr) == 0) {
return cur;
}
}
return found->rule;
return NULL;
}
int acl_truncate(acl_t *acl)
void acl_truncate(acl_t *acl)
{
if (acl == NULL) {
return ACL_ERROR;
}
/* Destroy all rules. */
skip_destroy_list(&acl->rules, 0, free);
skip_destroy_list(&acl->rules_pref, 0, free);
acl->rules = skip_create_list(acl_compare);
acl->rules_pref = skip_create_list(acl_compare);
if (acl->rules == NULL || acl->rules_pref == NULL) {
return ACL_ERROR;
return;
}
return ACL_ACCEPT;
WALK_LIST_FREE(*acl);
}
......@@ -20,8 +20,8 @@
*
* \brief Access control lists.
*
* An access control list is a named structure
* for efficient IP address and port matching.
* Simple access control list is implemented as a linked list, sorted by
* prefix length. This way, longest prefix match is always found first.
*
* \addtogroup common_lib
* @{
......@@ -30,46 +30,26 @@
#ifndef _KNOTD_ACL_H_
#define _KNOTD_ACL_H_
#include "common/skip-list.h"
#include "common/lists.h"
#include "common/sockaddr.h"
/*! \brief ACL rules types. */
typedef enum acl_rule_t {
ACL_ERROR = -1,
ACL_DENY = 0,
ACL_ACCEPT = 1
} acl_rule_t;
/*! \brief ACL flags. */
enum acl_flag_t {
ACL_PREFER = 1 << 0 /* Preferred node. */
};
/*! \brief ACL structure. */
typedef struct acl_t {
acl_rule_t default_rule; /*!< \brief Default rule. */
skip_list_t *rules; /*!< \brief Data container. */
skip_list_t *rules_pref; /*!< \brief Preferred data container. */
char name[]; /*!< \brief ACL semantic name. */
} acl_t;
typedef list acl_t;
/*! \brief Single ACL value. */
typedef struct acl_key_t {
/*! \brief Single ACL match. */
typedef struct acl_match {
node n;
sockaddr_t addr; /*!< \brief Address for comparison. */
acl_rule_t rule; /*!< \brief Rule for address. */
void *val; /*!< \brief Associated value (or NULL). */
} acl_key_t;
} acl_match_t;
/*!
* \brief Create a new ACL.
*
* \param default_rule Default rule for address matching.
* \param name ACL symbolic name (or NULL).
*
* \retval New ACL instance when successful.
* \retval NULL on errors.
*/
acl_t *acl_new(acl_rule_t default_rule, const char *name);
acl_t *acl_new();
/*!
* \brief Delete ACL structure.
......@@ -79,34 +59,28 @@ acl_t *acl_new(acl_rule_t default_rule, const char *name);
void acl_delete(acl_t **acl);
/*!
* \brief Create new ACL rule.
*
* \todo Support address subnets (issue #1366).
* \brief Insert new ACL match.
*
* \param acl Pointer to ACL instance.
* \param addr IP address.
* \param rule Rule for given address.
* \param val Value to be stored for given address (or NULL).
* \param flags Bitfield of ACL flags.
*
* \retval ACL_ACCEPT if successful.
* \retval ACP_ERROR on error.
* \retval KNOT_EOK if successful.
* \retval KNOT_EINVAL
* \retval KNOT_ENOMEM
*/
int acl_create(acl_t *acl, const sockaddr_t* addr, acl_rule_t rule, void *val,
unsigned flags);
int acl_insert(acl_t *acl, const sockaddr_t *addr, void *val);
/*!
* \brief Match address against ACL.
*
* \param acl Pointer to ACL instance.
* \param addr IP address.
* \param key Set to related key or NULL if not found.
*
* \retval Address rule if the address is accepted.
* \retval Default rule if the address is not accepted.
* \retval ACP_ERROR on error.
* \retval Matching rule instance if found.
* \retval NULL if it didn't find a match.
*/
int acl_match(acl_t *acl, const sockaddr_t* addr, acl_key_t **key);
acl_match_t* acl_find(acl_t *acl, const sockaddr_t *addr);
/*!
* \brief Truncate ACL.
......@@ -114,41 +88,8 @@ int acl_match(acl_t *acl, const sockaddr_t* addr, acl_key_t **key);
* All but the default rule will be dropped.
*
* \param acl Pointer to ACL instance.
*
* \retval ACL_ACCEPT if successful.
* \retval ACP_ERROR on error.
*/
int acl_truncate(acl_t *acl);
/*!
* \brief Return ACL name.
*
* \param acl Pointer to ACL instance.
*
* \retval ACL name.
*/
static inline const char* acl_name(acl_t *acl) {
if (!acl) {
return 0;
}
return acl->name;
}
/*!
* \brief Return ACL rules.
*
* \param acl Pointer to ACL instance.
*
* \retval ACL rules skip-list.
*/
static inline skip_list_t* acl_rules(acl_t *acl) {
if (!acl) {
return 0;
}
return acl->rules;
}
void acl_truncate(acl_t *acl);
#endif /* _KNOTD_ACL_H_ */
......
......@@ -348,7 +348,7 @@ static int conf_process(conf_t *conf)
sockaddr_init(&addr, -1);
sockaddr_set(&addr, i->family, i->address, 0);
sockaddr_setprefix(&addr, i->prefix);
acl_create(conf->ctl.acl, &addr, ACL_ACCEPT, i, 0);
acl_insert(conf->ctl.acl, &addr, i);
}
return ret;
......@@ -533,7 +533,7 @@ conf_t *conf_new(const char* path)
c->logs_count = -1;
/* ACLs. */
c->ctl.acl = acl_new(ACL_DENY, "remote_ctl");
c->ctl.acl = acl_new();
if (!c->ctl.acl) {
free(c->filename);
free(c);
......
......@@ -617,12 +617,12 @@ int remote_process(server_t *s, conf_iface_t *ctl_if, int r,
sockaddr_tostr(&a, straddr, sizeof(straddr));
int rport = sockaddr_portnum(&a);
knot_tsig_key_t *k = NULL;
acl_key_t *m = NULL;
acl_match_t *m = NULL;
knot_rcode_t ts_rc = 0;
uint16_t ts_trc = 0;
uint64_t ts_tmsigned = 0;
const knot_rrset_t *tsig_rr = knot_packet_tsig(pkt);
if (acl_match(conf()->ctl.acl, &a, &m) == ACL_DENY) {
if ((m = acl_find(conf()->ctl.acl, &a)) == NULL) {
knot_packet_free(&pkt);
log_server_warning("Denied remote control for '%s@%d' "
"(doesn't match ACL).\n",
......@@ -630,8 +630,7 @@ int remote_process(server_t *s, conf_iface_t *ctl_if, int r,
remote_senderr(c, buf, wire_len);
socket_close(c);
return KNOT_EACCES;
}
if (m && m->val) {
} else if (m->val) {
k = ((conf_iface_t *)m->val)->key;
}
......
......@@ -193,7 +193,7 @@ static int notify_check_and_schedule(knot_nameserver_t *nameserver,
/* Check ACL for notify-in. */
zonedata_t *zd = (zonedata_t *)knot_zone_data(zone);
if (from) {
if (acl_match(zd->notify_in, from, 0) == ACL_DENY) {
if (acl_find(zd->notify_in, from) == NULL) {
/* rfc1996: Ignore request and report incident. */
return KNOT_EDENIED;
}
......
......@@ -499,7 +499,7 @@ static int zones_set_acl(acl_t **acl, list* acl_list)
acl_delete(acl);
/* Create new ACL. */
*acl = acl_new(ACL_DENY, 0);
*acl = acl_new();
if (*acl == NULL) {
return KNOT_ENOMEM;
}
......@@ -519,15 +519,7 @@ static int zones_set_acl(acl_t **acl, list* acl_list)
/* Load rule. */
if (ret > 0) {
/*! \todo Correct search for the longest prefix match.
* This just favorizes remotes with TSIG.
* (issue #1675)
*/
unsigned flags = 0;
if (cfg_if->key != NULL) {
flags = ACL_PREFER;
}
acl_create(*acl, &addr, ACL_ACCEPT, cfg_if, flags);
acl_insert(*acl, &addr, cfg_if);
}
}
......@@ -1988,8 +1980,8 @@ int zones_query_check_zone(const knot_zone_t *zone, uint8_t q_opcode,
if (q_opcode == KNOT_OPCODE_UPDATE) {
acl_used = zd->update_in;
}
acl_key_t *match = NULL;
if (acl_match(acl_used, addr, &match) == ACL_DENY) {
acl_match_t *match = NULL;
if ((match = acl_find(acl_used, addr)) == NULL) {
*rcode = KNOT_RCODE_REFUSED;
return KNOT_EACCES;
} else {
......@@ -1997,12 +1989,9 @@ int zones_query_check_zone(const knot_zone_t *zone, uint8_t q_opcode,
"'%s %s'. match=%p\n", zd->conf->name,
q_opcode == KNOT_OPCODE_UPDATE ? "UPDATE":"XFR/OUT",
match);
if (match) {
if (match->val) {
/* Save configured TSIG key for comparison. */
conf_iface_t *iface = (conf_iface_t*)(match->val);
dbg_zones_detail("iface=%p, iface->key=%p\n",
iface, iface->key);
*tsig_key = iface->key;
*tsig_key = ((conf_iface_t*)(match->val))->key;
}
}
return KNOT_EOK;
......
......@@ -18,6 +18,7 @@
#include <sys/types.h>
#include <sys/socket.h>
#include "common/errcode.h"
#include "tests/common/acl_tests.h"
#include "common/sockaddr.h"
#include "common/acl.h"
......@@ -35,13 +36,14 @@ unit_api acl_tests_api = {
static int acl_tests_count(int argc, char *argv[])
{
return 20;
return 19;
}
static int acl_tests_run(int argc, char *argv[])
{
// 1. Create an ACL
acl_t *acl = acl_new(ACL_DENY, "simple ACL");
acl_match_t *match = NULL;
acl_t *acl = acl_new();
ok(acl != 0, "acl: new");
// 2. Create IPv4 address
......@@ -55,109 +57,109 @@ static int acl_tests_run(int argc, char *argv[])
ok(ret > 0, "acl: new IPv6 address");
// 4. Create simple IPv4 rule
ret = acl_create(acl, &test_v4, ACL_ACCEPT, 0, 0);
ok(ret == ACL_ACCEPT, "acl: inserted IPv4 rule");
ret = acl_insert(acl, &test_v4, NULL);
ok(ret == KNOT_EOK, "acl: inserted IPv4 rule");
// 5. Create simple IPv6 rule
ret = acl_create(acl, &test_v6, ACL_ACCEPT, 0, 0);
ok(ret == ACL_ACCEPT, "acl: inserted IPv6 rule");
ret = acl_insert(acl, &test_v6, NULL);
ok(ret == KNOT_EOK, "acl: inserted IPv6 rule");
// 6. Create simple IPv4 'any port' rule
sockaddr_t test_v4a;
sockaddr_set(&test_v4a, AF_INET, "20.20.20.20", 0);
ret = acl_create(acl, &test_v4a, ACL_ACCEPT, 0, 0);
ok(ret == ACL_ACCEPT, "acl: inserted IPv4 'any port' rule");
ret = acl_insert(acl, &test_v4a, NULL);
ok(ret == KNOT_EOK, "acl: inserted IPv4 'any port' rule");
// 7. Attempt to match unmatching address
sockaddr_t unmatch_v4;
sockaddr_set(&unmatch_v4, AF_INET, "10.10.10.10", 24424);
ret = acl_match(acl, &unmatch_v4, 0);
ok(ret == ACL_DENY, "acl: matching non-existing address");
match = acl_find(acl, &unmatch_v4);
ok(match == NULL, "acl: matching non-existing address");
// 8. Attempt to match unmatching IPv6 address
sockaddr_t unmatch_v6;
sockaddr_set(&unmatch_v6, AF_INET6, "2001:db8::1428:57ab", 24424);
ret = acl_match(acl, &unmatch_v6, 0);
ok(ret == ACL_DENY, "acl: matching non-existing IPv6 address");
match = acl_find(acl, &unmatch_v6);
ok(match == NULL, "acl: matching non-existing IPv6 address");
// 9. Attempt to match matching address
ret = acl_match(acl, &test_v4, 0);
ok(ret == ACL_ACCEPT, "acl: matching existing address");
match = acl_find(acl, &test_v4);
ok(match != NULL, "acl: matching existing address");
// 10. Attempt to match matching address
ret = acl_match(acl, &test_v6, 0);
ok(ret == ACL_ACCEPT, "acl: matching existing IPv6 address");
match = acl_find(acl, &test_v6);
ok(match != NULL, "acl: matching existing IPv6 address");
// 11. Attempt to match matching 'any port' address
sockaddr_t match_v4a;
sockaddr_set(&match_v4a, AF_INET, "20.20.20.20", 24424);
ret = acl_match(acl, &match_v4a, 0);
ok(ret == ACL_ACCEPT, "acl: matching existing IPv4 'any port' address");
match = acl_find(acl, &match_v4a);
ok(match != NULL, "acl: matching existing IPv4 'any port' address");
// 12. Attempt to match matching address without matching port
skip(1, 1) {
sockaddr_set(&unmatch_v4, AF_INET, "127.0.0.1", 54321);
ret = acl_match(acl, &unmatch_v4, 0);
ok(ret == ACL_DENY, "acl: matching address without matching port");
match = acl_find(acl, &unmatch_v4);
ok(match == NULL, "acl: matching address without matching port");
} endskip;
// 13. Invalid parameters
lives_ok({
acl_delete(0);
acl_create(0, 0, ACL_ERROR, 0, 0);
acl_match(0, 0, 0);
acl_insert(0, 0, NULL);
acl_find(0, 0);
acl_truncate(0);
acl_name(0);
}, "acl: won't crash with NULL parameters");
// 14. Attempt to match subnet
sockaddr_t match_pf4, test_pf4;
sockaddr_set(&match_pf4, AF_INET, "192.168.1.0", 0);
sockaddr_setprefix(&match_pf4, 24);
acl_create(acl, &match_pf4, ACL_ACCEPT, 0, 0);
acl_insert(acl, &match_pf4, NULL);
sockaddr_set(&test_pf4, AF_INET, "192.168.1.20", 0);
ret = acl_match(acl, &test_pf4, 0);
ok(ret == ACL_ACCEPT, "acl: searching address in matching prefix /24");
match = acl_find(acl, &test_pf4);
ok(match != NULL, "acl: searching address in matching prefix /24");
// 15. Attempt to search non-matching subnet
sockaddr_set(&test_pf4, AF_INET, "192.168.2.20", 0);
ret = acl_match(acl, &test_pf4, 0);
ok(ret == ACL_DENY, "acl: searching address in non-matching prefix /24");
match = acl_find(acl, &test_pf4);
ok(match == NULL, "acl: searching address in non-matching prefix /24");
// 16. Attempt to match v6 subnet
sockaddr_t match_pf6, test_pf6;
sockaddr_set(&match_pf6, AF_INET6, "2001:0DB8:0400:000e:0:0:0:AB00", 0);
sockaddr_setprefix(&match_pf6, 120);
acl_create(acl, &match_pf6, ACL_ACCEPT, 0, 0);
acl_insert(acl, &match_pf6, NULL);
sockaddr_set(&test_pf6, AF_INET6, "2001:0DB8:0400:000e:0:0:0:AB03", 0);
ret = acl_match(acl, &test_pf6, 0);
ok(ret == ACL_ACCEPT, "acl: searching v6 address in matching prefix /120");
match = acl_find(acl, &test_pf6);
ok(match != NULL, "acl: searching v6 address in matching prefix /120");
// 17. Attempt to search non-matching subnet
sockaddr_set(&test_pf6, AF_INET6, "2001:0DB8:0400:000e:0:0:0:CCCC", 0);
ret = acl_match(acl, &test_pf6, 0);
ok(ret == ACL_DENY, "acl: searching v6 address in non-matching prefix /120");
match = acl_find(acl, &test_pf6);
ok(match == NULL, "acl: searching v6 address in non-matching prefix /120");
// 18. Add preferred node
sockaddr_set(&test_pf4, AF_INET, "192.168.1.20", 0);
sockaddr_set(&test_pf4, AF_INET, "192.168.0.0", 0);
sockaddr_setprefix(&test_pf4, 16);
acl_insert(acl, &test_pf4, NULL);
sockaddr_set(&match_pf4, AF_INET, "192.168.1.20", 0);
void *sval = (void*)0x1234;
acl_create(acl, &match_pf4, ACL_ACCEPT, sval, ACL_PREFER);
acl_create(acl, &match_pf4, ACL_ACCEPT, 0, 0); /* Make decoy. */
acl_key_t *rval = NULL;
ret = acl_match(acl, &test_pf4, &rval);
ok(rval && rval->val == sval, "acl: search for preferred node");
acl_insert(acl, &match_pf4, sval);
match = acl_find(acl, &match_pf4);
ok(match && match->val == sval, "acl: search for preferred node");
// 19. Scenario after truncating
ok(acl_truncate(acl) == ACL_ACCEPT, "acl: truncate");
acl_truncate(acl);
sockaddr_set(&test_pf6, AF_INET6, "2001:a1b0:e11e:50d1::3:300", 0);
acl_create(acl, &test_pf6, ACL_ACCEPT, 0, 0);
acl_insert(acl, &test_pf6, NULL);
sockaddr_set(&test_pf4, AF_INET, "231.17.67.223", 0);
acl_create(acl, &test_pf4, ACL_ACCEPT, 0, 0);
acl_insert(acl, &test_pf4, NULL);
sockaddr_set(&test_pf4, AF_INET, "82.87.48.136", 0);
acl_create(acl, &test_pf4, ACL_ACCEPT, 0, 0);
acl_insert(acl, &test_pf4, NULL);
sockaddr_set(&match_pf4, AF_INET, "82.87.48.136", 12345);
ret = acl_match(acl, &match_pf4, 0);
ok(ret == ACL_ACCEPT, "acl: scenario after truncating");
match = acl_find(acl, &match_pf4);
ok(match != NULL, "acl: scenario after truncating");
acl_delete(&acl);
// Return
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment