From bd0c910830808b6d039bc4ac2655a95efd3991fe Mon Sep 17 00:00:00 2001 From: mb Date: Wed, 4 Nov 2020 17:00:28 +0100 Subject: [PATCH] RPZ: provide rpz-client-ip trigger and actions --- services/localzone.c | 4 +- services/rpz.c | 314 +++++++++++++++++++++++++++++--------- services/rpz.h | 1 + testdata/rpz_clientip.rpl | 180 ++++++++++++++++++++++ 4 files changed, 427 insertions(+), 72 deletions(-) create mode 100644 testdata/rpz_clientip.rpl diff --git a/services/localzone.c b/services/localzone.c index b5d8472bc..5e2104a7c 100644 --- a/services/localzone.c +++ b/services/localzone.c @@ -1561,7 +1561,7 @@ local_zones_zone_answer(struct local_zone* z, struct module_env* env, lz_type == local_zone_truncate)? LDNS_RCODE_NOERROR:LDNS_RCODE_NXDOMAIN; rcode = lz_type == local_zone_truncate ? (rcode|BIT_TC) : rcode; - if(z->soa) + if(z != NULL && z->soa) return local_encode(qinfo, env, edns, repinfo, buf, temp, z->soa, 0, rcode); local_error_encode(qinfo, env, edns, repinfo, buf, temp, rcode, @@ -1578,7 +1578,7 @@ local_zones_zone_answer(struct local_zone* z, struct module_env* env, * does not, then we should make this noerror/nodata */ if(ld && ld->rrsets) { int rcode = LDNS_RCODE_NOERROR; - if(z->soa) + if(z != NULL && z->soa) return local_encode(qinfo, env, edns, repinfo, buf, temp, z->soa, 0, rcode); local_error_encode(qinfo, env, edns, repinfo, buf, temp, rcode, diff --git a/services/rpz.c b/services/rpz.c index fb047a7f2..f8fa803de 100644 --- a/services/rpz.c +++ b/services/rpz.c @@ -51,6 +51,8 @@ #include "util/locks.h" #include "util/regional.h" +typedef struct resp_addr rpz_aclnode_type; + /** string for RPZ action enum */ const char* rpz_action_to_string(enum rpz_action a) @@ -305,6 +307,7 @@ void rpz_delete(struct rpz* r) return; local_zones_delete(r->local_zones); respip_set_delete(r->respip_set); + respip_set_delete(r->client_set); regional_destroy(r->region); free(r->taglist); free(r->log_name); @@ -317,12 +320,16 @@ rpz_clear(struct rpz* r) /* must hold write lock on auth_zone */ local_zones_delete(r->local_zones); respip_set_delete(r->respip_set); + respip_set_delete(r->client_set); if(!(r->local_zones = local_zones_create())){ return 0; } if(!(r->respip_set = respip_set_create())) { return 0; } + if(!(r->client_set = respip_set_create())) { + return 0; + } return 1; } @@ -332,6 +339,10 @@ rpz_finish_config(struct rpz* r) lock_rw_wrlock(&r->respip_set->lock); addr_tree_init_parents(&r->respip_set->ip_tree); lock_rw_unlock(&r->respip_set->lock); + + lock_rw_wrlock(&r->client_set->lock); + addr_tree_init_parents(&r->client_set->ip_tree); + lock_rw_unlock(&r->client_set->lock); } /** new rrset containing CNAME override, does not yet contain a dname */ @@ -398,6 +409,12 @@ rpz_create(struct config_auth* p) if(!(r->respip_set = respip_set_create())) { goto err; } + + /* (ab)use respip for client acl */ + if(!(r->client_set = respip_set_create())) { + goto err; + } + r->taglistlen = p->rpz_taglistlen; r->taglist = memdup(p->rpz_taglist, r->taglistlen); if(p->rpz_action_override) { @@ -440,6 +457,8 @@ err: local_zones_delete(r->local_zones); if(r->respip_set) respip_set_delete(r->respip_set); + if(r->client_set) + respip_set_delete(r->client_set); if(r->taglist) free(r->taglist); if(r->region) @@ -541,57 +560,95 @@ rpz_insert_qname_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen, return; } +static int +rpz_insert_ipaddr_based_trigger(struct respip_set* set, struct sockaddr_storage* addr, + socklen_t addrlen, int net, enum rpz_action a, uint16_t rrtype, + uint16_t rrclass, uint32_t ttl, uint8_t* rdata, size_t rdata_len, + uint8_t* rr, size_t rr_len) +{ + struct resp_addr* node; + char* rrstr; + enum respip_action respa = rpz_action_to_respip_action(a); + + lock_rw_wrlock(&set->lock); + rrstr = sldns_wire2str_rr(rr, rr_len); + if(!rrstr) { + log_err("malloc error while inserting RPZ respip trigger"); + lock_rw_unlock(&set->lock); + return 0; + } + if(!(node=respip_sockaddr_find_or_create(set, addr, addrlen, + net, 1, rrstr))) { + lock_rw_unlock(&set->lock); + free(rrstr); + return 0; + } + + lock_rw_wrlock(&node->lock); + lock_rw_unlock(&set->lock); + node->action = respa; + + if(a == RPZ_LOCAL_DATA_ACTION) { + respip_enter_rr(set->region, node, rrtype, + rrclass, ttl, rdata, rdata_len, rrstr, ""); + } + lock_rw_unlock(&node->lock); + free(rrstr); + return 1; +} + +static int +rpz_insert_client_ip_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen, + enum rpz_action a, uint16_t rrtype, uint16_t rrclass, uint32_t ttl, + uint8_t* rdata, size_t rdata_len, uint8_t* rr, size_t rr_len) +{ + struct sockaddr_storage addr; + socklen_t addrlen; + int net, af; + enum respip_action respa = rpz_action_to_respip_action(a); + + verbose(VERB_ALGO, "RPZ: insert client ip trigger: %s", rpz_action_to_string(a)); + if(a == RPZ_INVALID_ACTION || respa == respip_invalid) { + verbose(VERB_ALGO, "RPZ: skipping unsupported action: %s", + rpz_action_to_string(a)); + return 0; + } + + if(!netblockdnametoaddr(dname, dnamelen, &addr, &addrlen, &net, &af)) { + verbose(VERB_ALGO, "RPZ: unable to parse client ip"); + return 0; + } + + return rpz_insert_ipaddr_based_trigger(r->client_set, &addr, addrlen, net, + a, rrtype, rrclass, ttl, rdata, rdata_len, rr, rr_len); +} + /** Insert RR into RPZ's respip_set */ static int rpz_insert_response_ip_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen, enum rpz_action a, uint16_t rrtype, uint16_t rrclass, uint32_t ttl, uint8_t* rdata, size_t rdata_len, uint8_t* rr, size_t rr_len) { - struct resp_addr* node; struct sockaddr_storage addr; socklen_t addrlen; int net, af; - char* rrstr; enum respip_action respa = rpz_action_to_respip_action(a); + verbose(VERB_ALGO, "RPZ: insert response ip trigger: %s", rpz_action_to_string(a)); + if(a == RPZ_INVALID_ACTION || respa == respip_invalid) { verbose(VERB_ALGO, "RPZ: skipping unsupported action: %s", rpz_action_to_string(a)); return 0; } - if(a == RPZ_TCP_ONLY_ACTION) { - verbose(VERB_ALGO, "RPZ: insert respip trigger: tcp-only"); - } - - if(!netblockdnametoaddr(dname, dnamelen, &addr, &addrlen, &net, &af)) - return 0; - - lock_rw_wrlock(&r->respip_set->lock); - rrstr = sldns_wire2str_rr(rr, rr_len); - if(!rrstr) { - log_err("malloc error while inserting RPZ respip trigger"); - lock_rw_unlock(&r->respip_set->lock); - return 0; - } - if(!(node=respip_sockaddr_find_or_create(r->respip_set, &addr, addrlen, - net, 1, rrstr))) { - lock_rw_unlock(&r->respip_set->lock); - free(rrstr); + if(!netblockdnametoaddr(dname, dnamelen, &addr, &addrlen, &net, &af)) { + verbose(VERB_ALGO, "RPZ: unable to parse response ip"); return 0; } - lock_rw_wrlock(&node->lock); - lock_rw_unlock(&r->respip_set->lock); - node->action = respa; - - if(a == RPZ_LOCAL_DATA_ACTION) { - respip_enter_rr(r->respip_set->region, node, rrtype, - rrclass, ttl, rdata, rdata_len, rrstr, ""); - } - lock_rw_unlock(&node->lock); - free(rrstr); - return 1; + return rpz_insert_ipaddr_based_trigger(r->respip_set, &addr, addrlen, net, + a, rrtype, rrclass, ttl, rdata, rdata_len, rr, rr_len); } int @@ -643,14 +700,17 @@ rpz_insert_rr(struct rpz* r, uint8_t* azname, size_t aznamelen, uint8_t* dname, rpz_insert_qname_trigger(r, policydname, policydnamelen, a, rr_type, rr_class, rr_ttl, rdatawl, rdatalen, rr, rr_len); - } - else if(t == RPZ_RESPONSE_IP_TRIGGER) { + } else if(t == RPZ_RESPONSE_IP_TRIGGER) { rpz_insert_response_ip_trigger(r, policydname, policydnamelen, a, rr_type, rr_class, rr_ttl, rdatawl, rdatalen, rr, rr_len); free(policydname); - } - else { + } else if(t == RPZ_CLIENT_IP_TRIGGER) { + rpz_insert_client_ip_trigger(r, policydname, policydnamelen, + a, rr_type, rr_class, rr_ttl, rdatawl, rdatalen, rr, + rr_len); + free(policydname); + } else { free(policydname); verbose(VERB_ALGO, "RPZ: skipping unsupported trigger: %s", rpz_trigger_to_string(t)); @@ -954,6 +1014,124 @@ log_rpz_apply(uint8_t* dname, enum rpz_action a, struct query_info* qinfo, log_nametypeclass(0, txt, qinfo->qname, qinfo->qtype, qinfo->qclass); } +static enum rpz_action +rpz_apply_client_ip_trigger(struct rpz* r, struct comm_reply* repinfo) +{ + struct resp_addr* raddr = NULL; + enum rpz_action action = RPZ_INVALID_ACTION; + struct sockaddr_storage* addr = &repinfo->addr; + socklen_t addrlen = repinfo->addrlen; + + lock_rw_rdlock(&r->client_set->lock); + + raddr = (struct resp_addr*)addr_tree_lookup(&r->client_set->ip_tree, + addr, addrlen); + if(raddr != NULL) { + action = respip_action_to_rpz_action(raddr->action); + lock_rw_unlock(&raddr->lock); + } + + verbose(VERB_ALGO, "RPZ: apply client ip trigger: found=%d action=%s", + raddr != NULL, rpz_action_to_string(action)); + + lock_rw_unlock(&r->client_set->lock); + + return action; +} + +static inline +enum rpz_action +rpz_resolve_client_action_and_zone(struct auth_zones* az, struct query_info* qinfo, + struct comm_reply* repinfo, uint8_t* taglist, size_t taglen, + struct ub_server_stats* stats, + /* output parameters */ + struct local_zone** z_out, struct auth_zone** a_out, struct rpz** r_out ) +{ + struct auth_zone* a = NULL; + struct rpz* r = NULL; + struct local_zone* z = NULL; + enum rpz_action action = RPZ_PASSTHRU_ACTION; + + lock_rw_rdlock(&az->rpz_lock); + + for(a = az->rpz_first; a; a = a->rpz_az_next) { + lock_rw_rdlock(&a->lock); + r = a->rpz; + if(r->taglist && !taglist_intersect(r->taglist, + r->taglistlen, taglist, taglen)) { + lock_rw_unlock(&a->lock); + continue; + } + z = rpz_find_zone(r, qinfo->qname, qinfo->qname_len, + qinfo->qclass, 0, 0, 0); + action = rpz_apply_client_ip_trigger(r, repinfo); + if(z && r->action_override == RPZ_DISABLED_ACTION) { + if(r->log) + log_rpz_apply(z->name, + r->action_override, + qinfo, repinfo, r->log_name); + /* TODO only register stats when stats_extended? */ + stats->rpz_action[r->action_override]++; + lock_rw_unlock(&z->lock); + z = NULL; + } + if(z) { + break; + } + /* not found in this auth_zone */ + lock_rw_unlock(&a->lock); + } + + lock_rw_unlock(&az->rpz_lock); + + *r_out = r; + *a_out = a; + *z_out = z; + + return action; +} + +static inline int +rpz_resolve_final_localzone_action(struct rpz* r, struct local_zone* z, enum rpz_action client_action) +{ + enum localzone_type lzt; + if(r->action_override == RPZ_NO_OVERRIDE_ACTION) { + switch (client_action) { + case RPZ_NODATA_ACTION: + case RPZ_NXDOMAIN_ACTION: + case RPZ_DROP_ACTION: + case RPZ_TCP_ONLY_ACTION: + case RPZ_PASSTHRU_ACTION: + lzt = rpz_action_to_localzone_type(client_action); + break; + case RPZ_LOCAL_DATA_ACTION: + verbose(VERB_ALGO, + "RPZ: client ip trigger with local-data unimplemented:" + " defaulting to rpz-passthru"); + lzt = rpz_action_to_localzone_type(RPZ_PASSTHRU_ACTION); + break; + case RPZ_INVALID_ACTION: + lzt = z->type; + break; + default: + lzt = z->type; + break; + } + } else { + lzt = rpz_action_to_localzone_type(r->action_override); + } + return lzt; +} + +static inline int +rpz_is_udp_query(struct comm_reply* repinfo) { + return repinfo != NULL + ? (repinfo->c != NULL + ? repinfo->c->type == comm_udp + : 0) + : 0; +} + int rpz_apply_qname_trigger(struct auth_zones* az, struct module_env* env, struct query_info* qinfo, struct edns_data* edns, sldns_buffer* buf, @@ -961,45 +1139,41 @@ rpz_apply_qname_trigger(struct auth_zones* az, struct module_env* env, uint8_t* taglist, size_t taglen, struct ub_server_stats* stats) { struct rpz* r = NULL; - struct auth_zone* a; - int ret; - enum localzone_type lzt; + struct auth_zone* a = NULL; struct local_zone* z = NULL; struct local_data* ld = NULL; - lock_rw_rdlock(&az->rpz_lock); - for(a = az->rpz_first; a; a = a->rpz_az_next) { - lock_rw_rdlock(&a->lock); - r = a->rpz; - if(!r->taglist || taglist_intersect(r->taglist, - r->taglistlen, taglist, taglen)) { - z = rpz_find_zone(r, qinfo->qname, qinfo->qname_len, - qinfo->qclass, 0, 0, 0); - if(z && r->action_override == RPZ_DISABLED_ACTION) { - if(r->log) - log_rpz_apply(z->name, - r->action_override, - qinfo, repinfo, r->log_name); - /* TODO only register stats when stats_extended? - * */ - stats->rpz_action[r->action_override]++; - lock_rw_unlock(&z->lock); - z = NULL; - } - if(z) - break; - } - lock_rw_unlock(&a->lock); /* not found in this auth_zone */ - } - lock_rw_unlock(&az->rpz_lock); + int ret; + enum localzone_type lzt; + enum rpz_action client_action; - if(!z) - return 0; /* not holding auth_zone.lock anymore */ + client_action = rpz_resolve_client_action_and_zone( + az, qinfo, repinfo, taglist, taglen, stats, &z, &a, &r); + + verbose(VERB_ALGO, "RPZ: qname trigger: client action=%s", + rpz_action_to_string(client_action)); + + if(!z) { + verbose(VERB_ALGO, "RPZ: client action without zone"); + if(client_action == RPZ_PASSTHRU_ACTION + || client_action == RPZ_INVALID_ACTION + || (client_action == RPZ_TCP_ONLY_ACTION + && !rpz_is_udp_query(repinfo))) { + return 0; + } + // XXX: log_rpz_apply not possbile because no zone + stats->rpz_action[client_action]++; + local_zones_zone_answer(NULL /*no zone*/, env, qinfo, edns, + repinfo, buf, temp, 0 /* no local data used */, + rpz_action_to_localzone_type(client_action)); + return 1; + } log_assert(r); - if(r->action_override == RPZ_NO_OVERRIDE_ACTION) - lzt = z->type; - else - lzt = rpz_action_to_localzone_type(r->action_override); + + lzt = rpz_resolve_final_localzone_action(r, z, client_action); + + verbose(VERB_ALGO, "RPZ: final client action=%s", + rpz_action_to_string(localzone_type_to_rpz_action(lzt))); if(r->action_override == RPZ_CNAME_OVERRIDE_ACTION) { qinfo->local_alias = @@ -1040,7 +1214,7 @@ rpz_apply_qname_trigger(struct auth_zones* az, struct module_env* env, lock_rw_unlock(&a->lock); return !qinfo->local_alias; } -verbose(VERB_ALGO, "xxxxxx repinfo=%p is_udp=%d", repinfo, repinfo->c->type == comm_udp); + ret = local_zones_zone_answer(z, env, qinfo, edns, repinfo, buf, temp, 0 /* no local data used */, lzt); if(r->log) diff --git a/services/rpz.h b/services/rpz.h index 77a2db55c..1210f2039 100644 --- a/services/rpz.h +++ b/services/rpz.h @@ -92,6 +92,7 @@ enum rpz_action { struct rpz { struct local_zones* local_zones; struct respip_set* respip_set; + struct respip_set* client_set; uint8_t* taglist; size_t taglistlen; enum rpz_action action_override; diff --git a/testdata/rpz_clientip.rpl b/testdata/rpz_clientip.rpl new file mode 100644 index 000000000..bd559871b --- /dev/null +++ b/testdata/rpz_clientip.rpl @@ -0,0 +1,180 @@ +; config options +server: + module-config: "respip validator iterator" + target-fetch-policy: "0 0 0 0 0" + qname-minimisation: no + access-control: 192.0.0.0/8 allow + +rpz: + name: "rpz.example.com." + zonefile: +TEMPFILE_NAME rpz.example.com +TEMPFILE_CONTENTS rpz.example.com +$ORIGIN example.com. +rpz 3600 IN SOA ns1.rpz.example.com. hostmaster.rpz.example.com. ( + 1379078166 28800 7200 604800 7200 ) + 3600 IN NS ns1.rpz.example.com. + 3600 IN NS ns2.rpz.example.com. +$ORIGIN rpz.example.com. +24.0.0.0.192.rpz-client-ip CNAME . +24.0.1.0.192.rpz-client-ip CNAME *. +24.0.2.0.192.rpz-client-ip CNAME rpz-drop. +24.0.3.0.192.rpz-client-ip CNAME rpz-passthru. +24.0.4.0.192.rpz-client-ip CNAME rpz-tcp-only. +TEMPFILE_END + +stub-zone: + name: "a." + stub-addr: 10.20.30.40 +CONFIG_END + +SCENARIO_BEGIN Test RPZ client ip triggers + +RANGE_BEGIN 0 100 + ADDRESS 10.20.30.40 +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +a. IN NS +SECTION ANSWER +a. IN NS ns.a. +SECTION ADDITIONAL +ns.a IN A 10.20.30.40 +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +ADJUST copy_id +REPLY QR NOERROR +SECTION QUESTION +a.a. IN TXT +SECTION ANSWER +a.a. IN TXT "upstream txt rr a.a." +ENTRY_END + +RANGE_END + +; unrelated client ip address -- passthru + +STEP 10 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +a.a. IN TXT +ENTRY_END + +STEP 11 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA NOERROR +SECTION QUESTION +a.a. IN TXT +SECTION ANSWER +a.a. IN TXT "upstream txt rr a.a." +ENTRY_END + +; should be NXDOMAIN + +STEP 20 QUERY ADDRESS 192.0.0.1 +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +a.a. IN TXT +ENTRY_END + +STEP 21 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR AA RD RA NXDOMAIN +SECTION QUESTION +a.a. IN TXT +SECTION ANSWER +ENTRY_END + +; should be NODATA + +STEP 30 QUERY ADDRESS 192.0.1.1 +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +a.a. IN TXT +ENTRY_END + +STEP 31 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR AA RD RA NOERROR +SECTION QUESTION +a.a. IN TXT +SECTION ANSWER +ENTRY_END + +; should be PASSTHRU + +STEP 40 QUERY ADDRESS 192.0.3.1 +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +a.a. IN TXT +ENTRY_END + +STEP 41 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA NOERROR +SECTION QUESTION +a.a. IN TXT +SECTION ANSWER +a.a. IN TXT "upstream txt rr a.a." +ENTRY_END + +; should be TRUNCATED + +STEP 50 QUERY ADDRESS 192.0.4.1 +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +a.a. IN TXT +ENTRY_END + +STEP 51 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR AA TC RD RA NOERROR +SECTION QUESTION +a.a. IN TXT +SECTION ANSWER +ENTRY_END + +; should not be TRUNCATED via TCP + +STEP 52 QUERY ADDRESS 192.0.4.1 +ENTRY_BEGIN +MATCH TCP +REPLY RD +SECTION QUESTION +a.a. IN TXT +ENTRY_END + +STEP 53 CHECK_ANSWER +ENTRY_BEGIN +MATCH all TCP +REPLY QR RD RA NOERROR +SECTION QUESTION +a.a. IN TXT +SECTION ANSWER +a.a. IN TXT "upstream txt rr a.a." +ENTRY_END + +; should be DROPPED + +STEP 90 QUERY ADDRESS 192.0.2.1 +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +a.a. IN TXT +ENTRY_END + +SCENARIO_END