├── .github └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum └── plugin ├── firewall ├── firewall.go ├── firewall_test.go ├── policy │ ├── engine.go │ ├── expression.go │ └── expression_test.go ├── rule │ ├── list.go │ └── list_test.go ├── setup.go └── setup_test.go ├── opa ├── README.md ├── opa.go ├── opa_test.go ├── setup.go └── setup_test.go ├── pkg ├── response │ └── reader.go └── rqdata │ ├── rqdata.go │ └── rqdata_test.go └── themis ├── README.md ├── attrholder.go ├── attrholder_test.go ├── attributes.go ├── attributes_test.go ├── client.go ├── client └── builtin_client.go ├── client_test.go ├── config.go ├── config_test.go ├── dns_test.go ├── metrics.go ├── metrics_test.go ├── pool.go ├── pool_test.go ├── setup.go └── themis.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | 9 | - name: Install Go 10 | uses: actions/setup-go@v2 11 | with: 12 | go-version: '1.17.0' 13 | id: go 14 | 15 | - name: Check out code 16 | uses: actions/checkout@v2 17 | 18 | - name: Build 19 | run: go build -v ./... 20 | 21 | - name: Test 22 | run: go test -race ./... 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.4 5 | 6 | script: 7 | - go test ./... 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firewall 2 | 3 | ## Name 4 | 5 | *firewall* - enables filtering of queries and responses based on expressions. 6 | 7 | ## Description 8 | 9 | The firewall plugin defines a list of rules that trigger a workflow action on a DNS query and its response. 10 | A rule list is an ordered set of rules that are evaluated in sequence. 11 | Rules can be an expression rule, or a policy engine rule. 12 | An expression rule has two parts: an action and an expression. When the rule is evaluated, 13 | first the expression is evaluated. 14 | - If the expression evaluates to `true` the action is performed on the query and the rule list evaluation ceases. 15 | - If the expression does not evaluates to `true` then next rule in sequence is evaluated. 16 | 17 | The firewall plugin can also refer to other policy engines to determine the action to take. 18 | 19 | ## Syntax 20 | 21 | ~~~ txt 22 | firewall DIRECTION { 23 | ACTION EXPRESSION 24 | POLICY-PLUGIN ENGINE-NAME 25 | } 26 | ~~~~ 27 | 28 | * **DIRECTION** indicates if the _rule list_ will be applied to queries or responses. It can be `query` or `response`. 29 | 30 | * **ACTION** defines the workflow action to take if the **EXPRESSION** evaluates to `true`. If no actions are defined 31 | for the `query` **DIRECTION**, the default action is to `block`. 32 | Available actions: 33 | - `allow` : continue the DNS resolution process (i.e., continue to the next plugin in the chain) 34 | - `refuse` : interrupt the DNS resolution, reply with REFUSED response code 35 | - `block` : interrupt the DNS resolution, reply with NXDOMAIN response code 36 | - `drop` : interrupt the DNS resolution, do not reply (client will time out) 37 | 38 | An action must be followed by an **EXPRESSION**, which defines the boolean expression for the rule. See Expressions 39 | section below. 40 | 41 | * **POLICY-PLUGIN** : is the name of another plugin that implements a firewall policy engine. 42 | **ENGINE-NAME** is the name of an engine defined in your Corefile. Requests/responses will be evaluated by 43 | that plugin policy engine to determine the action. 44 | 45 | ## Expressions 46 | 47 | Expressions follow a [c-like expression format](https://github.com/Knetic/govaluate/blob/master/MANUAL.md) where the variables are either 48 | the `metadata` of CoreDNS or the fields of a DNS query/response. Common operators apply. 49 | 50 | Expression Examples: 51 | * `client_ip == '10.0.0.20'` 52 | * `type == 'AAAA'` 53 | * `type IN ('AAAA', 'A', 'TXT')` 54 | * `type IN ('AAAA', 'A') && name =~ 'google.com'` 55 | * `[mac/address] =~ '.*:FF:.*'` 56 | 57 | NOTE: because of the `/` separator included in a label of metadata, those labels must be enclosed in 58 | brackets `[...]`. 59 | 60 | ### Expression Variables 61 | 62 | The following variables are supported for querying information in expressions. All values are strings (see `atoi` function 63 | below for arithmetic operations). 64 | 65 | * `type`: type of the request (A, AAAA, TXT, ...) 66 | * `name`: name of the request (the domain name requested) 67 | * `class`: class of the request (IN, CH, ...) 68 | * `proto`: protocol used (tcp or udp) 69 | * `client_ip`: client's IP address, for IPv6 addresses these are enclosed in brackets: `[::1]` 70 | * `size`: request size in bytes 71 | * `port`: client's port 72 | * `rcode`: response CODE (NOERROR, NXDOMAIN, SERVFAIL, ...) 73 | * `rsize`: raw (uncompressed), response size (a client may receive a smaller response) 74 | * `>rflags`: response flags, each set flag will be displayed, e.g., "aa, tc". This includes the qr 75 | bit as well 76 | * `>bufsize`: the EDNS0 buffer size advertised in the query 77 | * `>do`: the EDNS0 DO (DNSSEC OK) bit set in the query 78 | * `>id`: query ID 79 | * `>opcode`: query OPCODE 80 | * `server_ip`: server's IP address; for IPv6 addresses these are enclosed in brackets: `[::1]` 81 | * `server_port` : client's port 82 | * `response_ip` : the IP address returned in the first A or AAAA record of the Answer section 83 | 84 | ### Expression Functions 85 | 86 | * `atoi(string)`: convert a string to a numeric value. 87 | * `incidr(ip, cidr)`: returns true if `ip` is in the subnet defined by `cidr`. 88 | * `random()`: returns a random floating point number in the range [0.0, 1.0). 89 | 90 | ## Policy Engine Plugins 91 | 92 | In addition to using the built-in action/expression syntax, the _firewall_ plugin can use a policy engine plugin 93 | to evaluate policy. 94 | 95 | To use a policy engine plugin, you'll need to compile the plugin into CoreDNS, then declare the plugin in your 96 | Corefile, and reference the plugin as an action of a firewall rule. See the "Using a Policy Engine Plugin" example below. 97 | 98 | When authoring a new policy engine plugin, the plugin must implement the `Engineer` interface defined in firewall/policy. 99 | 100 | This repository includes two Policy Engine Plugins: 101 | * *themis* - enables Infoblox's Themis policy engine to be used as a CoreDNS firewall policy engine 102 | * *opa* - enables OPA to be used as a CoreDNS firewall policy engine. 103 | 104 | ## External Plugin 105 | 106 | *Firewall* and other associated policy plugins in this repository are *external* plugins, which means they are not included in CoreDNS releases. 107 | To use the plugins in this repository, you'll need to build a CoreDNS image with the plugins you want to add included in the plugin list. In a nutshell you'll need to: 108 | * Clone release, e.g., ` git clone -b v1.6.9 --depth 1 https://github.com/coredns/coredns .` 109 | * Add this plugin (`firewall:github.com/coredns/policy/plugin/firewall`), and any desired policy engines to [plugin.cfg](https://github.com/coredns/coredns/blob/master/plugin.cfg) per instructions therein. Order in this file is important, as it dictates the order of plugin execution when processing a query. The firewall plugin should execute before plugins that generate responses. 110 | * `make -f Makefile.release DOCKER=your-docker-repo release` 111 | * `make -f Makefile.release DOCKER=your-docker-repo docker` 112 | * `make -f Makefile.release DOCKER=your-docker-repo docker-push` 113 | 114 | ## Examples 115 | 116 | ### Client IP ACL 117 | This example shows how to use *firewall* to create a basic client IP address-based ACL. Here `10.120.1.11` is expressly REFUSED. 118 | Other clients in `10.120.0.0/24` and `10.120.1.0/24` are allowed. All other clients are not responded to. 119 | 120 | ~~~ corefile 121 | . { 122 | firewall query { 123 | refuse client_ip == '10.120.1.11' 124 | allow client_ip =~ '10\.120\.0\..*' 125 | allow client_ip =~ '10\.120\.1\..*' 126 | drop true 127 | } 128 | } 129 | ~~~ 130 | 131 | ### Domain Name Policy 132 | Allow all queries for example.com. 133 | Allow A and AAAA queries for google.com. 134 | NXDOMAIN all other queries. 135 | 136 | ~~~ corefile 137 | . { 138 | firewall query { 139 | allow name =~ 'example.com' 140 | allow name =~ 'google.com' && (type == 'A' || type == 'AAAA') 141 | block true 142 | } 143 | } 144 | ~~~ 145 | 146 | ### Response Policy 147 | Allow all queries, but block all responses that contain an IP matching `10.120.1.*` in the first record 148 | of the Answer section. 149 | 150 | ~~~ corefile 151 | . { 152 | firewall query { 153 | allow true 154 | } 155 | firewall response { 156 | block response_ip =~ '10\.120\.1\..*' 157 | } 158 | } 159 | ~~~ 160 | 161 | ### EDNS0 Metadata Policy 162 | This example uses the *metadata_edns0* plugin to define labels `group_id` and `client_id` with values extracted from EDNS0. 163 | The firewall rules use those metadata to REFUSE any query without a group_id of `123456789` or client_id of `ABCDEF`. 164 | 165 | ~~~ corefile 166 | example.org { 167 | metadata 168 | metadata_edns0 { 169 | group_id edns0 0xffed bytes 170 | client_id edns0 0xffee bytes 171 | } 172 | firewall query { 173 | refuse [metadata_edns0/client_id] != 'ABCDEF' 174 | refuse [metadata_edns0/group_id] != '123456789' 175 | allow true 176 | } 177 | } 178 | ~~~ 179 | 180 | ### Kubernetes Metadata Multi-Tenancy Policy 181 | This example shows how *firewall* could be useful in a Kubernetes-based multi-tenancy application. It uses the *kubernetes* 182 | plugin metadata to prevent Pods in certain Namespaces from looking up Services in other Namespaces. 183 | Specifically, if the requesting Pod is in a Namespace beginning with 'tenant-', it permits only lookups to 184 | Services that are in the same Namespace or in the 'default' Namespace. Note here that `pods verified` is 185 | required in the *kubernetes* plugin to enable the use of the `[kubernetes/client-namespace]` metadata variable. Also note that 186 | this is at the DNS level only, and does not prevent network access between tenant Namespaces, e.g., if Service IPs are known. 187 | 188 | ~~~ corefile 189 | cluster.local { 190 | metadata 191 | kubernetes { 192 | pods verified 193 | } 194 | firewall query { 195 | allow [kubernetes/client-namespace] !~ '^tenant-' 196 | allow [kubernetes/namespace] == [kubernetes/client-namespace] 197 | allow [kubernetes/namespace] == 'default' 198 | block true 199 | } 200 | } 201 | ~~~ 202 | 203 | ### Using a Policy Engine Plugin 204 | 205 | The following example illustrates how the a policy engine plugin (*themis* in this example) can be used by the *firewall* plugin. 206 | 207 | ~~~ 208 | . { 209 | firewall query { 210 | themis myengine 211 | } 212 | 213 | themis myengine { 214 | 215 | } 216 | } 217 | ~~~ 218 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coredns/policy 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible 7 | github.com/coredns/caddy v1.1.0 8 | github.com/coredns/coredns v1.8.4 9 | github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 10 | github.com/infobloxopen/themis v0.0.5 11 | github.com/miekg/dns v1.1.42 12 | github.com/prometheus/client_golang v1.10.0 13 | github.com/prometheus/client_model v0.2.0 14 | ) 15 | -------------------------------------------------------------------------------- /plugin/firewall/firewall.go: -------------------------------------------------------------------------------- 1 | // Package firewall enables filtering on query and response using direct expression as policy. 2 | // it allows interact with other Policy Engines if those are plugin implementing the Engineer interface 3 | package firewall 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | 9 | "github.com/coredns/coredns/plugin" 10 | "github.com/coredns/coredns/plugin/pkg/nonwriter" 11 | "github.com/coredns/coredns/request" 12 | "github.com/coredns/policy/plugin/firewall/policy" 13 | "github.com/coredns/policy/plugin/firewall/rule" 14 | "github.com/coredns/policy/plugin/pkg/response" 15 | 16 | "github.com/miekg/dns" 17 | ) 18 | 19 | var ( 20 | errInvalidAction = errors.New("invalid action") 21 | ) 22 | 23 | // ExpressionEngineName is the name associated with built-in rules of Expression type. 24 | const ExpressionEngineName = "--default--" 25 | 26 | // firewall represents a plugin instance that can validate DNS 27 | // requests and replies using rulelists on the query and/or on the reply 28 | type firewall struct { 29 | engines map[string]policy.Engine 30 | query *rule.List 31 | reply *rule.List 32 | 33 | next plugin.Handler 34 | } 35 | 36 | //New build a new firewall plugin 37 | func New() (*firewall, error) { 38 | pol := &firewall{engines: map[string]policy.Engine{"--default--": policy.NewExprEngine()}} 39 | var err error 40 | if pol.query, err = rule.NewList(policy.TypeBlock, false); err != nil { 41 | return nil, err 42 | } 43 | if pol.reply, err = rule.NewList(policy.TypeAllow, true); err != nil { 44 | return nil, err 45 | } 46 | return pol, nil 47 | } 48 | 49 | // ServeDNS implements the Handler interface. 50 | func (p *firewall) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { 51 | var ( 52 | status = -1 53 | respMsg *dns.Msg 54 | errfw error 55 | queryData = make(map[string]interface{}, 0) 56 | ) 57 | 58 | state := request.Request{W: w, Req: r} 59 | 60 | // evaluate query to determine action 61 | action, err := p.query.Evaluate(ctx, state, queryData, p.engines) 62 | if err != nil { 63 | m := new(dns.Msg) 64 | m = m.SetRcode(r, dns.RcodeServerFailure) 65 | w.WriteMsg(m) 66 | return dns.RcodeSuccess, err 67 | } 68 | 69 | if action == policy.TypeAllow { 70 | // if Allow : ask next plugin to resolve the DNS query 71 | // temp writer: hold the DNS response until evaluation of the Reply Rulelist 72 | writer := nonwriter.New(w) 73 | // RequestDataExtractor requires a response.Reader to be able to evaluate the information on the DNS response 74 | reader := response.NewReader(writer) 75 | 76 | // ask other plugins to resolve 77 | _, err := plugin.NextOrFailure(p.Name(), p.next, ctx, reader, r) 78 | if err != nil { 79 | m := new(dns.Msg) 80 | m = m.SetRcode(r, dns.RcodeServerFailure) 81 | w.WriteMsg(m) 82 | return dns.RcodeSuccess, err 83 | } 84 | respMsg = writer.Msg 85 | 86 | stateReply := request.Request{W: reader, Req: respMsg} 87 | 88 | // whatever the response, send to the Reply RuleList for action 89 | action, err = p.reply.Evaluate(ctx, stateReply, queryData, p.engines) 90 | if err != nil { 91 | m := new(dns.Msg) 92 | m = m.SetRcode(r, dns.RcodeServerFailure) 93 | w.WriteMsg(m) 94 | return dns.RcodeSuccess, err 95 | } 96 | } 97 | 98 | // Now apply the action evaluated by the RuleLists 99 | switch action { 100 | case policy.TypeAllow: 101 | // the response from next plugin, whatever it is, is good to go 102 | w.WriteMsg(respMsg) 103 | return dns.RcodeSuccess, nil 104 | case policy.TypeBlock: 105 | // One of the RuleList ended evaluation with typeBlock : return the initial request with corresponding rcode 106 | status = dns.RcodeNameError 107 | case policy.TypeRefuse: 108 | // One of the RuleList ended evaluation with typeRefuse : return the initial request with corresponding rcode 109 | status = dns.RcodeRefused 110 | case policy.TypeDrop: 111 | // One of the RuleList ended evaluation with typeDrop : simulate a drop 112 | return dns.RcodeSuccess, nil 113 | default: 114 | // Any other action returned by RuleLists is considered an internal error 115 | status = dns.RcodeServerFailure 116 | errfw = errInvalidAction 117 | } 118 | m := new(dns.Msg) 119 | m.SetRcode(r, status) 120 | if errfw == nil { 121 | w.WriteMsg(m) 122 | } 123 | return dns.RcodeSuccess, errfw 124 | } 125 | 126 | // Name implements the Handler interface. 127 | func (p *firewall) Name() string { return "firewall" } 128 | -------------------------------------------------------------------------------- /plugin/firewall/firewall_test.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/coredns/coredns/plugin" 8 | "github.com/coredns/coredns/plugin/test" 9 | "github.com/coredns/policy/plugin/firewall/policy" 10 | "github.com/coredns/policy/plugin/pkg/response" 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | // NextHandler returns a Handler that returns rcode and err. 15 | func ProcessHandler(rcode int, err error) plugin.Handler { 16 | return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { 17 | if err != nil { 18 | return dns.RcodeServerFailure, nil 19 | } 20 | 21 | answer := new(dns.Msg) 22 | answer.SetRcode(r, rcode) 23 | 24 | w.WriteMsg(answer) 25 | 26 | return rcode, nil 27 | }) 28 | } 29 | 30 | func TestFirewallResolution(t *testing.T) { 31 | 32 | tests := []struct { 33 | queryFilter int 34 | replyFilter int 35 | next int 36 | resultCode int 37 | msgCode int 38 | msgNil bool 39 | }{ 40 | // This all works because 1 bucket (1 zone, 1 type) 41 | {policy.TypeDrop, policy.TypeAllow, dns.RcodeSuccess, dns.RcodeSuccess, dns.RcodeSuccess, true}, 42 | {policy.TypeRefuse, policy.TypeAllow, dns.RcodeSuccess, dns.RcodeSuccess, dns.RcodeRefused, false}, 43 | {policy.TypeBlock, policy.TypeAllow, dns.RcodeSuccess, dns.RcodeSuccess, dns.RcodeNameError, false}, 44 | {policy.TypeAllow, policy.TypeAllow, dns.RcodeSuccess, dns.RcodeSuccess, dns.RcodeSuccess, false}, 45 | {policy.TypeAllow, policy.TypeRefuse, dns.RcodeSuccess, dns.RcodeSuccess, dns.RcodeRefused, false}, 46 | {policy.TypeAllow, policy.TypeBlock, dns.RcodeSuccess, dns.RcodeSuccess, dns.RcodeNameError, false}, 47 | {policy.TypeAllow, policy.TypeDrop, dns.RcodeSuccess, dns.RcodeSuccess, dns.RcodeSuccess, true}, 48 | } 49 | 50 | ctx := context.TODO() 51 | for i, tc := range tests { 52 | 53 | // prepare firewall parameters 54 | fw, _ := New() 55 | fw.query.DefaultPolicy = tc.queryFilter 56 | fw.reply.DefaultPolicy = tc.replyFilter 57 | fw.next = ProcessHandler(tc.next, nil) 58 | 59 | //create a msg 60 | req := new(dns.Msg) 61 | req.SetQuestion("example.com", dns.TypeA) 62 | 63 | rec := response.NewReader(&test.ResponseWriter{}) 64 | rcode, err := fw.ServeDNS(ctx, rec, req) 65 | if err != nil { 66 | t.Fatalf("Test %d: Expected no error, but got %s", i, err) 67 | } 68 | 69 | // now check expectation 70 | 71 | if rcode != tc.resultCode { 72 | t.Errorf("Test %d: Expected value %s as return code, but got %s", i, dns.RcodeToString[tc.resultCode], dns.RcodeToString[rcode]) 73 | } 74 | 75 | if rec.Msg != nil && rec.Msg.Rcode != tc.msgCode { 76 | t.Errorf("Test %d: Expected value %s as DNS reply code, but got %s", i, dns.RcodeToString[tc.msgCode], dns.RcodeToString[rec.Msg.Rcode]) 77 | } 78 | 79 | if (rec.Msg == nil) != tc.msgNil { 80 | t.Errorf("Test %d: Expected MSG to be return as NIL : %v, but got a NIL : %v", i, tc.msgNil, rec.Msg == nil) 81 | } 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /plugin/firewall/policy/engine.go: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/coredns/coredns/request" 7 | ) 8 | 9 | const ( 10 | // TypeNone policy action is UNKNOWN: no decision, apply the next rule. 11 | TypeNone = iota 12 | // TypeRefuse policy action is REFUSE: do not resolve a query and return code REFUSED 13 | TypeRefuse 14 | // TypeAllow policy action is ALLOW: continue to resolve query 15 | TypeAllow 16 | // TypeBlock policy action is BLOCK: do not resolve a query and return code NXDOMAIN 17 | TypeBlock 18 | // TypeDrop policy action is DROP: do not resolve a query and simulate a lost query 19 | TypeDrop 20 | 21 | // TypeCount total number of actions allowed 22 | TypeCount 23 | ) 24 | 25 | // NameTypes keep a mapping of the byte constant to the corresponding name 26 | var NameTypes = map[int]string{ 27 | TypeNone: "none", 28 | TypeAllow: "allow", 29 | TypeRefuse: "refuse", 30 | TypeBlock: "block", 31 | TypeDrop: "drop", 32 | } 33 | 34 | // Rule defines a policy for continuing DNS query processing. 35 | type Rule interface { 36 | // Evaluate the rule and return one of the TypeXXX defined above 37 | // - TypeNone should be returned if the Rule is not able to decide any action for this query 38 | // - otherwise return one of TypeAllow/TypeRefuse/TypeDrop/TypeBlock 39 | Evaluate(data interface{}) (int, error) 40 | } 41 | 42 | // Engine for Firewall plugin 43 | type Engine interface { 44 | // BuildRules - create a Rule based on args or throw an error, This Rule will be evaluated during processing of DNS Queries 45 | BuildRule(args []string) (Rule, error) // create a rule based on parameters 46 | 47 | //BuildQueryData generate the data needed to evaluate - for one query - ALL the rules of this Engine 48 | BuildQueryData(ctx context.Context, state request.Request) (interface{}, error) 49 | 50 | //BuildReplyData generate the data needed to evaluate - for one response - ALL the rules of this Engine 51 | BuildReplyData(ctx context.Context, state request.Request, queryData interface{}) (interface{}, error) 52 | } 53 | 54 | // Engineer allow registration of Policy Engines. One plugin can declare several Engines. 55 | type Engineer interface { 56 | Engine(name string) Engine 57 | } 58 | -------------------------------------------------------------------------------- /plugin/firewall/policy/expression.go: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/coredns/coredns/plugin/metadata" 12 | "github.com/coredns/coredns/request" 13 | "github.com/coredns/policy/plugin/pkg/rqdata" 14 | 15 | expr "github.com/Knetic/govaluate" 16 | ) 17 | 18 | type ruleExpr struct { 19 | action int 20 | actionIfError int 21 | expression *expr.EvaluableExpression 22 | } 23 | 24 | // ExprEngine implement interface Engine for Firewall plugin 25 | // it evaluate the rues using an the lib Knetic/govaluate 26 | type ExprEngine struct { 27 | actionIfErrorEvaluation int 28 | dataFromReq *rqdata.Mapping 29 | } 30 | 31 | type dataAsParam struct { 32 | ctx context.Context 33 | dataFromReq *rqdata.Extractor 34 | } 35 | 36 | // NewExprEngine create a new Engine with default configuration 37 | func NewExprEngine() *ExprEngine { 38 | return &ExprEngine{TypeRefuse, rqdata.NewMapping("")} 39 | } 40 | 41 | //BuildQueryData here return a dataAsParam that can be used by to evaluate the variables of the expression 42 | func (x *ExprEngine) BuildQueryData(ctx context.Context, state request.Request) (interface{}, error) { 43 | return &dataAsParam{ctx, rqdata.NewExtractor(state, x.dataFromReq)}, nil 44 | } 45 | 46 | //BuildReplyData here return a dataAsParam that can be used by to evaluate the variables of the expression 47 | func (x *ExprEngine) BuildReplyData(ctx context.Context, state request.Request, query interface{}) (interface{}, error) { 48 | return &dataAsParam{ctx, rqdata.NewExtractor(state, x.dataFromReq)}, nil 49 | } 50 | 51 | //BuildRule create a rule for Expression Engine: 52 | // - first param is one of the action to return 53 | // - second and following param is a sentence the represent an Expression 54 | func (x *ExprEngine) BuildRule(args []string) (Rule, error) { 55 | keyword := args[0] 56 | exp := args[1:] 57 | e, err := expr.NewEvaluableExpressionWithFunctions(strings.Join(exp, " "), map[string]expr.ExpressionFunction{ 58 | "atoi": atoi, 59 | "incidr": incidr, 60 | "random": random, 61 | }) 62 | if err != nil { 63 | return nil, fmt.Errorf("cannot create a valid expression : %s", err) 64 | } 65 | 66 | var kind = TypeNone 67 | for k, n := range NameTypes { 68 | if keyword == n { 69 | kind = k 70 | } 71 | } 72 | if kind == TypeNone { 73 | return nil, fmt.Errorf("invalid keyword %s for a policy rule", keyword) 74 | } 75 | return &ruleExpr{kind, x.actionIfErrorEvaluation, e}, nil 76 | } 77 | 78 | func random(arguments ...interface{}) (interface{}, error) { 79 | if len(arguments) != 0 { 80 | return nil, fmt.Errorf("invalid number of arguments") 81 | } 82 | return rand.Float64(), nil 83 | } 84 | 85 | func atoi(arguments ...interface{}) (interface{}, error) { 86 | if len(arguments) != 1 { 87 | return nil, fmt.Errorf("atoi requires exactly one string argument") 88 | } 89 | s, ok := arguments[0].(string) 90 | if !ok { 91 | return nil, fmt.Errorf("atoi requires exactly one string argument") 92 | } 93 | n, err := strconv.Atoi(s) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return float64(n), nil 98 | } 99 | 100 | func incidr(args ...interface{}) (interface{}, error) { 101 | if len(args) != 2 { 102 | return nil, fmt.Errorf("invalid number of arguments") 103 | } 104 | ip := net.ParseIP(args[0].(string)) 105 | if ip == nil { 106 | return nil, fmt.Errorf("first argument is not an IP address") 107 | } 108 | _, cidr, err := net.ParseCIDR(args[1].(string)) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return cidr.Contains(ip), nil 113 | } 114 | 115 | func toBoolean(v interface{}) (bool, error) { 116 | if s, ok := v.(string); ok { 117 | return strings.ToLower(s) == "true", nil 118 | } 119 | if b, ok := v.(bool); ok { 120 | return b, nil 121 | } 122 | if i, ok := v.(int); ok { 123 | return i != 0, nil 124 | } 125 | return false, fmt.Errorf("cannot extract a boolean value from result of expression") 126 | } 127 | 128 | //Evaluate the current expression, using data as a variable resolver for Expression 129 | func (r *ruleExpr) Evaluate(data interface{}) (int, error) { 130 | 131 | params, ok := data.(*dataAsParam) 132 | if !ok { 133 | return r.actionIfError, fmt.Errorf("evaluation of expression '%s' - params provided are of wrong type, expect a go Context", r.expression.String()) 134 | } 135 | res, err := r.expression.Eval(params) 136 | if err != nil { 137 | return r.actionIfError, fmt.Errorf("evaluation of expression '%s' return an error : %s", r.expression.String(), err) 138 | } 139 | result, err := toBoolean(res) 140 | if err != nil { 141 | return r.actionIfError, fmt.Errorf("evaluation of expression '%s' return an non boolean value : %s", r.expression.String(), err) 142 | } 143 | 144 | if result { 145 | return r.action, nil 146 | } 147 | return TypeNone, nil 148 | } 149 | 150 | // Get return the value associated with the variable 151 | // required by the interface of Knetic/govaluate for evaluation of the 'variables' in the expression 152 | // DataRequestExtractor is evaluated first, and if the name does not match then metadata is evaluated 153 | func (p *dataAsParam) Get(name string) (interface{}, error) { 154 | v, exist := p.dataFromReq.Value(name) 155 | if exist { 156 | return v, nil 157 | } 158 | f := metadata.ValueFunc(p.ctx, name) 159 | if f == nil { 160 | return "", nil 161 | } 162 | return f(), nil 163 | } 164 | -------------------------------------------------------------------------------- /plugin/firewall/policy/expression_test.go: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | 10 | tst "github.com/coredns/coredns/plugin/test" 11 | "github.com/coredns/coredns/request" 12 | 13 | "github.com/coredns/policy/plugin/pkg/response" 14 | "github.com/coredns/policy/plugin/pkg/rqdata" 15 | 16 | "github.com/miekg/dns" 17 | ) 18 | 19 | func TestBuildRule(t *testing.T) { 20 | tests := []struct { 21 | expression string 22 | errorBuild bool 23 | }{ 24 | {"allow true", false}, 25 | {"block 1 + 1", false}, 26 | {"drop [my/variable] / 20", false}, 27 | {"unknown", true}, 28 | {"untype 'expression'", true}, 29 | {"drop [my/variable / 20", true}, 30 | } 31 | for i, test := range tests { 32 | engine := &ExprEngine{TypeDrop, rqdata.NewMapping("-")} 33 | _, err := engine.BuildRule(strings.Split(test.expression, " ")) 34 | if err != nil { 35 | if !test.errorBuild { 36 | t.Errorf("Test %d : unexpected error at build rule : %s", i, err) 37 | } 38 | continue 39 | } 40 | if test.errorBuild { 41 | t.Errorf("Test %d : no error at BuilRule returned, when one was expected", i) 42 | } 43 | } 44 | } 45 | 46 | func TestToBoolean(t *testing.T) { 47 | tests := []struct { 48 | data interface{} 49 | value bool 50 | error bool 51 | }{ 52 | {"", false, false}, 53 | {false, false, false}, 54 | {"false", false, false}, 55 | {0, false, false}, 56 | {[]string{}, false, true}, 57 | {"whatever", false, false}, 58 | {true, true, false}, 59 | {"TRue", true, false}, 60 | {3, true, false}, 61 | {[]string{"whatever"}, true, true}, 62 | } 63 | 64 | for i, test := range tests { 65 | v, err := toBoolean(test.data) 66 | if err != nil { 67 | if !test.error { 68 | t.Errorf("Test %d : unexpected error at boolean evaluation : %s", i, err) 69 | } 70 | continue 71 | } 72 | if test.error { 73 | t.Errorf("Test %d : no error at boolean evaluation, when one was expected", i) 74 | continue 75 | } 76 | 77 | if v != test.value { 78 | t.Errorf("Test %d : value return is not the one expected - expected : %v, got : %v", i, test.value, v) 79 | } 80 | 81 | } 82 | } 83 | 84 | func TestRuleEvaluate(t *testing.T) { 85 | tests := []struct { 86 | expression string 87 | value bool 88 | errorExec bool 89 | }{ 90 | {"true", true, false}, 91 | {"type == 'HINFO'", true, false}, 92 | {"type == 'AAAA'", false, false}, 93 | {"name =~ 'org'", true, false}, 94 | {"atoi('4') == 4.0", true, false}, 95 | {"incidr('1.2.3.4','1.2.3.0/24')", true, false}, 96 | {"incidr('1:2:3:4::1','1:2:3:4::/32')", true, false}, 97 | {"random() < 1.0", true, false}, 98 | {"random() >= 0.0", true, false}, 99 | {"random('1') >= 0.0", true, true}, 100 | } 101 | for i, test := range tests { 102 | 103 | engine := &ExprEngine{TypeDrop, rqdata.NewMapping("-")} 104 | rule, err := engine.BuildRule(append([]string{NameTypes[TypeAllow]}, strings.Split(test.expression, " ")...)) 105 | if err != nil { 106 | t.Errorf("Test %d, expr : %s - unexpected error at build rule : %s", i, test.expression, err) 107 | continue 108 | } 109 | 110 | ctx := context.TODO() 111 | 112 | // build a Request 113 | r := new(dns.Msg) 114 | r.SetQuestion("example.org.", dns.TypeHINFO) 115 | r.MsgHdr.AuthenticatedData = true 116 | w := response.NewReader(&tst.ResponseWriter{}) 117 | state := request.Request{Req: r, W: w} 118 | 119 | data, err := engine.BuildQueryData(ctx, state) 120 | if err != nil { 121 | t.Errorf("Test %d, expr : %s - unexpected error at build query data : %s", i, test.expression, err) 122 | continue 123 | } 124 | result, err := rule.Evaluate(data) 125 | if err != nil { 126 | if !test.errorExec { 127 | t.Errorf("Test %d, expr : %s - unexpected error at evaluate : %s", i, test.expression, err) 128 | } 129 | continue 130 | } 131 | 132 | if (result == TypeAllow) != test.value { 133 | t.Errorf("Test %d, expr : %v - value return is not the one expected - expected : %v, got : %v", i, test.expression, test.value, (result == TypeAllow)) 134 | } 135 | 136 | } 137 | } 138 | 139 | func TestAtoi(t *testing.T) { 140 | tests := []struct { 141 | args []interface{} 142 | expected interface{} 143 | expectedErr error 144 | }{ 145 | { 146 | args: []interface{}{"42"}, 147 | expected: float64(42), 148 | }, 149 | { 150 | args: []interface{}{"42", "100"}, 151 | expectedErr: fmt.Errorf("atoi requires exactly one string argument"), 152 | }, 153 | { 154 | args: []interface{}{}, 155 | expectedErr: fmt.Errorf("atoi requires exactly one string argument"), 156 | }, 157 | { 158 | args: []interface{}{42}, 159 | expectedErr: fmt.Errorf("atoi requires exactly one string argument"), 160 | }, 161 | { 162 | args: []interface{}{"foo"}, 163 | expectedErr: fmt.Errorf("strconv.Atoi: parsing \"foo\": invalid syntax"), 164 | }, 165 | } 166 | for i, test := range tests { 167 | v, err := atoi(test.args...) 168 | if test.expectedErr != nil { 169 | if err == nil { 170 | t.Errorf("Test %d, args : %v - expected error - expected : %v, got : %v", i, test.args, test.expectedErr, nil) 171 | } else if err.Error() != test.expectedErr.Error() { 172 | t.Errorf("Test %d, args : %v - expected error - expected : %v, got : %v", i, test.args, test.expectedErr, err) 173 | } 174 | continue 175 | } 176 | 177 | if !reflect.DeepEqual(v, test.expected) { 178 | t.Errorf("Test %d, args : %v - value return is not the one expected - expected : %v, got : %v", i, test.args, test.expected, v) 179 | } 180 | } 181 | } 182 | 183 | func TestInCidr(t *testing.T) { 184 | tests := []struct { 185 | args []interface{} 186 | expected interface{} 187 | expectedErr error 188 | }{ 189 | { 190 | args: []interface{}{"1.2.3.4", "1.2.3.0/24"}, 191 | expected: true, 192 | }, 193 | { 194 | args: []interface{}{"1.2.3.4", "5.6.7.0/24"}, 195 | expected: false, 196 | }, 197 | { 198 | args: []interface{}{"1:2:3:4::1", "1:2:3:4::/32"}, 199 | expected: true, 200 | }, 201 | { 202 | args: []interface{}{"1:2:3:4::1", "5:6:7:8::/32"}, 203 | expected: false, 204 | }, 205 | { 206 | args: []interface{}{"1.2.3.4"}, 207 | expectedErr: fmt.Errorf("invalid number of arguments"), 208 | }, 209 | { 210 | args: []interface{}{"foo", "5.6.7.0/24"}, 211 | expectedErr: fmt.Errorf("first argument is not an IP address"), 212 | }, 213 | } 214 | for i, test := range tests { 215 | v, err := incidr(test.args...) 216 | if test.expectedErr != nil { 217 | if err == nil { 218 | t.Errorf("Test %d, args : %v - expected error - expected : %v, got : %v", i, test.args, test.expectedErr, nil) 219 | } else if err.Error() != test.expectedErr.Error() { 220 | t.Errorf("Test %d, args : %v - expected error - expected : %v, got : %v", i, test.args, test.expectedErr, err) 221 | } 222 | continue 223 | } 224 | 225 | if !reflect.DeepEqual(v, test.expected) { 226 | t.Errorf("Test %d, args : %v - value return is not the one expected - expected : %v, got : %v", i, test.args, test.expected, v) 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /plugin/firewall/rule/list.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/coredns/coredns/request" 9 | "github.com/coredns/policy/plugin/firewall/policy" 10 | ) 11 | 12 | //Element is a structure that host a definition of policy Rule, and the Rule itself when created 13 | type Element struct { 14 | Plugin string 15 | Name string 16 | Params []string 17 | Rule policy.Rule 18 | } 19 | 20 | //List of Rules checked in order of the list 21 | type List struct { 22 | Reply bool 23 | Rules []*Element 24 | DefaultPolicy int 25 | } 26 | 27 | // NewList to create an empty new List of Rules 28 | func NewList(ifNoResult int, isReply bool) (*List, error) { 29 | if ifNoResult >= policy.TypeCount { 30 | return nil, fmt.Errorf("invalid default rulelist parameters: %v", ifNoResult) 31 | } 32 | return &List{Reply: isReply, DefaultPolicy: ifNoResult}, nil 33 | } 34 | 35 | //Add the element at end of the list 36 | func (p *List) Add(e *Element) error { 37 | // verify that if any other Element has already the same name, it also has the same plugin 38 | for _, ex := range p.Rules { 39 | if ex.Name == e.Name { 40 | if ex.Plugin != e.Plugin { 41 | return fmt.Errorf("the Engine name '%s' is used by two different plugins: %s and %s", e.Name, e.Plugin, ex.Plugin) 42 | } 43 | } 44 | } 45 | p.Rules = append(p.Rules, e) 46 | return nil 47 | } 48 | 49 | //Engines list all engines involved - return map[name] -> plugin 50 | func (p *List) Engines() map[string]string { 51 | eng := make(map[string]string, len(p.Rules)) 52 | for _, re := range p.Rules { 53 | eng[re.Name] = re.Plugin 54 | } 55 | return eng 56 | } 57 | 58 | //BuildRules instanciate the Rule of each Element, based on the parameters collected from setup. 59 | func (p *List) BuildRules(engines map[string]policy.Engine) error { 60 | var err error 61 | for _, re := range p.Rules { 62 | if re.Rule == nil { 63 | e, ok := engines[re.Name] 64 | if !ok { 65 | return fmt.Errorf("unknown engine for Plugin %s and Name %s - cannot build the Rule", re.Plugin, re.Name) 66 | } 67 | re.Rule, err = e.BuildRule(re.Params) 68 | if err != nil { 69 | return fmt.Errorf("cannot build Rule for Plugin %s, Name %s and Params %s - error is %s", re.Plugin, re.Name, strings.Join(re.Params, ","), err) 70 | } 71 | } 72 | } 73 | return nil 74 | } 75 | 76 | func (p *List) buildQueryData(ctx context.Context, name string, state request.Request, data map[string]interface{}, engines map[string]policy.Engine) (interface{}, error) { 77 | if d, ok := data[name]; ok { 78 | return d, nil 79 | } 80 | if e, ok := engines[name]; ok { 81 | d, err := e.BuildQueryData(ctx, state) 82 | if err != nil { 83 | return nil, err 84 | } 85 | data[name] = d 86 | return d, nil 87 | } 88 | return nil, fmt.Errorf("unregistered engine instance %s", name) 89 | } 90 | 91 | func (p *List) buildReplyData(ctx context.Context, name string, state request.Request, queryData interface{}, data map[string]interface{}, engines map[string]policy.Engine) (interface{}, error) { 92 | if d, ok := data[name]; ok { 93 | return d, nil 94 | } 95 | if e, ok := engines[name]; ok { 96 | d, err := e.BuildReplyData(ctx, state, queryData) 97 | if err != nil { 98 | return nil, err 99 | } 100 | data[name] = d 101 | return d, nil 102 | } 103 | return nil, fmt.Errorf("unregistered engine instance %s", name) 104 | } 105 | 106 | //Evaluate all policy one by one until one provide a valid result 107 | //if no Rule can provide a result, the DefaultPolicy of the list applies 108 | func (p *List) Evaluate(ctx context.Context, state request.Request, data map[string]interface{}, engines map[string]policy.Engine) (int, error) { 109 | var dataReply = make(map[string]interface{}, 0) 110 | for i, r := range p.Rules { 111 | rd, err := p.buildQueryData(ctx, r.Name, state, data, engines) 112 | if err != nil { 113 | return policy.TypeNone, fmt.Errorf("rulelist Rule %v, with Name %s - cannot build query data for evaluation %s", i, r.Name, err) 114 | } 115 | if p.Reply { 116 | rd, err = p.buildReplyData(ctx, r.Name, state, rd, dataReply, engines) 117 | if err != nil { 118 | return policy.TypeNone, fmt.Errorf("rulelist Rule %v, with Name %s - cannot build Reply data for evaluation %s", i, r.Name, err) 119 | } 120 | } 121 | pr, err := r.Rule.Evaluate(rd) 122 | if err != nil { 123 | return policy.TypeNone, fmt.Errorf("rulelist Rule %v returned an error at evaluation %s", i, err) 124 | } 125 | if pr >= policy.TypeCount { 126 | return policy.TypeNone, fmt.Errorf("rulelist Rule %v returned an invalid value %v", i, pr) 127 | 128 | } 129 | if pr != policy.TypeNone { 130 | // Rule returned a valid value 131 | return pr, nil 132 | } 133 | // if no result just continue on next Rule 134 | } 135 | // if none of Rule make a statement, then we return the default policy 136 | return p.DefaultPolicy, nil 137 | } 138 | -------------------------------------------------------------------------------- /plugin/firewall/rule/list_test.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/coredns/coredns/plugin/test" 10 | "github.com/coredns/coredns/request" 11 | "github.com/coredns/policy/plugin/firewall/policy" 12 | 13 | "github.com/miekg/dns" 14 | ) 15 | 16 | // Stub Engine for test purposes 17 | 18 | type testEngine struct { 19 | error error 20 | result int 21 | } 22 | 23 | func (r *testEngine) Evaluate(data interface{}) (int, error) { 24 | return r.result, r.error 25 | } 26 | 27 | type stubEngine struct { 28 | name string 29 | alwaysError bool 30 | } 31 | 32 | func (e *stubEngine) BuildQueryData(ctx context.Context, state request.Request) (interface{}, error) { 33 | if e.alwaysError { 34 | return nil, fmt.Errorf("engine %s is returning an error at buildQueryData", e.name) 35 | } 36 | return e.name, nil 37 | } 38 | 39 | func (e *stubEngine) BuildReplyData(ctx context.Context, state request.Request, query interface{}) (interface{}, error) { 40 | if e.alwaysError { 41 | return nil, fmt.Errorf("engine %s is returning an error at buildReplyData", e.name) 42 | } 43 | return e.name, nil 44 | } 45 | 46 | func (e *stubEngine) BuildRule(args []string) (policy.Rule, error) { 47 | if e.alwaysError { 48 | return nil, fmt.Errorf("engine %s is returning an error at BuildRule", e.name) 49 | } 50 | r := policy.TypeNone 51 | var err error 52 | if len(args) > 0 { 53 | v, errconv := strconv.Atoi(args[0]) 54 | if errconv != nil { 55 | err = fmt.Errorf("Rule from %s, evalute as an error : %s", e.name, args[0]) 56 | } else { 57 | r = v 58 | } 59 | } 60 | return &testEngine{err, r}, nil 61 | } 62 | 63 | func TestEnsureRules(t *testing.T) { 64 | 65 | engines := map[string]policy.Engine{ 66 | "good": &stubEngine{"good", false}, 67 | "wrong": &stubEngine{"wrong", true}, 68 | } 69 | 70 | tests := []struct { 71 | rules []*Element 72 | error bool 73 | }{ 74 | // unknown engine 75 | {[]*Element{{"Plugin", "unknown", []string{}, nil}, 76 | {"Plugin", "good", []string{}, nil}}, 77 | true, 78 | }, 79 | // invalid Params 80 | {[]*Element{{"Plugin", "wrong", []string{}, nil}, 81 | {"Plugin", "good", []string{}, nil}}, 82 | true, 83 | }, 84 | // all ok 85 | {[]*Element{{"Plugin", "good", []string{}, nil}, 86 | {"Plugin", "good", []string{}, nil}}, 87 | false, 88 | }, 89 | } 90 | for i, test := range tests { 91 | rl, _ := NewList(policy.TypeDrop, false) 92 | rl.Rules = test.rules 93 | 94 | err := rl.BuildRules(engines) 95 | if err != nil { 96 | if !test.error { 97 | t.Errorf("Test %d : unexpected error at build Rule : %s", i, err) 98 | } 99 | continue 100 | } 101 | if test.error { 102 | t.Errorf("Test %d : no error at EnsureRules returned, when one was expected", i) 103 | } 104 | } 105 | } 106 | 107 | func TestEvaluate(t *testing.T) { 108 | 109 | engines := map[string]policy.Engine{ 110 | "good": &stubEngine{"good", false}, 111 | "wrong": &stubEngine{"wrong", true}, 112 | } 113 | 114 | tests := []struct { 115 | rules []*Element 116 | error bool 117 | value int 118 | }{ 119 | 120 | // error at query data 121 | {[]*Element{{"Plugin", "wrong", []string{}, nil}, 122 | {"Plugin", "good", []string{}, nil}}, 123 | true, policy.TypeNone, 124 | }, 125 | // error at Reply data 126 | {[]*Element{{"Plugin", "wrong", []string{}, nil}, 127 | {"Plugin", "good", []string{}, nil}}, 128 | true, policy.TypeNone, 129 | }, 130 | // error returned by evaluation 131 | {[]*Element{{"Plugin", "good", []string{"Error returned"}, nil}, 132 | {"Plugin", "good", []string{}, nil}}, 133 | true, policy.TypeNone, 134 | }, 135 | // invalid value returned by evaluation 136 | {[]*Element{{"Plugin", "good", []string{"123"}, nil}, 137 | {"Plugin", "good", []string{}, nil}}, 138 | true, policy.TypeNone, 139 | }, 140 | // a correct value is returned by the rulelist 141 | {[]*Element{ 142 | {"Plugin", "good", []string{"0"}, nil}, 143 | {"Plugin", "good", []string{"0"}, nil}, 144 | {"Plugin", "good", []string{"0"}, nil}, 145 | {"Plugin", "good", []string{"2"}, nil}}, 146 | false, policy.TypeAllow, 147 | }, 148 | // no value is returned by the rulelist 149 | {[]*Element{ 150 | {"Plugin", "good", []string{"0"}, nil}, 151 | {"Plugin", "good", []string{"0"}, nil}, 152 | {"Plugin", "good", []string{"0"}, nil}}, 153 | false, policy.TypeDrop, 154 | }, 155 | } 156 | for i, tst := range tests { 157 | rl, _ := NewList(policy.TypeDrop, false) 158 | rl.Rules = tst.rules 159 | rl.BuildRules(engines) 160 | 161 | state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} 162 | state.Req.SetQuestion("example.org.", dns.TypeA) 163 | 164 | ctx := context.TODO() 165 | data := make(map[string]interface{}) 166 | result, err := rl.Evaluate(ctx, state, data, engines) 167 | if err != nil { 168 | if !tst.error { 169 | t.Errorf("Test %d : unexpected error at Evaluate rulelist : %s", i, err) 170 | } 171 | continue 172 | } 173 | if tst.error { 174 | t.Errorf("Test %d : no error at Evaluate rulelist returned, when one was expected", i) 175 | continue 176 | } 177 | if result != tst.value { 178 | t.Errorf("Test %d : value return is not the one expected - expected : %v, got : %v", i, tst.value, result) 179 | continue 180 | } 181 | 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /plugin/firewall/setup.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/coredns/caddy" 8 | "github.com/coredns/coredns/core/dnsserver" 9 | "github.com/coredns/coredns/plugin" 10 | "github.com/coredns/policy/plugin/firewall/policy" 11 | "github.com/coredns/policy/plugin/firewall/rule" 12 | ) 13 | 14 | func init() { 15 | caddy.RegisterPlugin("firewall", caddy.Plugin{ 16 | ServerType: "dns", 17 | Action: setup, 18 | }) 19 | } 20 | 21 | func setup(c *caddy.Controller) error { 22 | fw, err := parse(c) 23 | 24 | if err != nil { 25 | return plugin.Error("firewall", err) 26 | } 27 | 28 | dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { 29 | fw.next = next 30 | return fw 31 | }) 32 | 33 | c.OnStartup(func() error { 34 | // after all plugin are setup, ensure to have all rules created by enrolling the engines pointed 35 | // by pending rules 36 | err := fw.enrollEngines(c) 37 | if err != nil { 38 | return err 39 | } 40 | for _, loc := range []*rule.List{fw.query, fw.reply} { 41 | // now that all engines are known, ensure to build the rules for each element of the lists 42 | err = loc.BuildRules(fw.engines) 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | return nil 48 | }) 49 | 50 | return nil 51 | } 52 | 53 | func parse(c *caddy.Controller) (*firewall, error) { 54 | p, err := New() 55 | if err != nil { 56 | return nil, fmt.Errorf("cannot create the firewall plugin structure, error : %e", err) 57 | } 58 | 59 | for c.Next() { 60 | opts := c.RemainingArgs() 61 | if len(opts) != 1 { 62 | return nil, c.Errf("one and only one paramater is expected after firewall : the location of the rulelist. It should be either query or reply") 63 | } 64 | location := opts[0] 65 | var rl *rule.List 66 | switch location { 67 | case "query": 68 | rl = p.query 69 | case "response": 70 | rl = p.reply 71 | default: 72 | return nil, c.Errf("invalid location of rule list: %s . It should be either query or response", location) 73 | } 74 | // check if location already used or not 75 | for c.NextBlock() { 76 | r, err := p.parseOptionOrRule(c) 77 | if err != nil { 78 | return nil, err 79 | } 80 | err = rl.Add(r) 81 | if err != nil { 82 | return nil, c.Errf("cannot add a rule to the %s list : %s", location, err) 83 | } 84 | } 85 | } 86 | return p, nil 87 | } 88 | 89 | func (p *firewall) parseOptionOrRule(c *caddy.Controller) (*rule.Element, error) { 90 | // by default, at least one engine is available : the ExpressionEngine 91 | e := policy.NewExprEngine() 92 | switch c.Val() { 93 | case policy.NameTypes[policy.TypeRefuse]: 94 | fallthrough 95 | case policy.NameTypes[policy.TypeAllow]: 96 | fallthrough 97 | case policy.NameTypes[policy.TypeBlock]: 98 | fallthrough 99 | case policy.NameTypes[policy.TypeDrop]: 100 | // these 4 direct policy action denotes the actions for the default Engine: ExpressionEngine 101 | action := c.Val() 102 | name := ExpressionEngineName 103 | args := c.RemainingArgs() 104 | if len(args) < 1 { 105 | return nil, fmt.Errorf("not enough arguments to build a policy rule, expect allow/refuse/block/drop query/reply , got %s %s", c.Val(), strings.Join(args, " ")) 106 | } 107 | params := append([]string{action}, args...) 108 | r, err := e.BuildRule(params) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return &rule.Element{"", name, params, r}, nil 113 | 114 | default: 115 | // we can only suppose it is an engine type(plugin name), name and args 116 | plugin := c.Val() 117 | args := c.RemainingArgs() 118 | if len(args) < 1 { 119 | return nil, fmt.Errorf("not enough arguments to build a policy rule, expect %s ", c.Val()) 120 | } 121 | name := args[0] 122 | params := args[1:] 123 | // as the Engine are not yet knowm, just create a ruleElement with the parameters.The Element will be created later 124 | return &rule.Element{plugin, name, params, nil}, nil 125 | 126 | } 127 | } 128 | 129 | func (p *firewall) enrollEngines(c *caddy.Controller) error { 130 | 131 | var eng = p.query.Engines() 132 | for n, p := range p.reply.Engines() { 133 | eng[n] = p 134 | } 135 | // remove Expression engine that is built-in 136 | delete(eng, ExpressionEngineName) 137 | 138 | // retrieve all needed Engines. 139 | // These are plugins that implements the 'Engineer' interface and are named in one of the firewall lists 140 | plugins := dnsserver.GetConfig(c).Handlers() 141 | for _, pl := range plugins { 142 | if e, ok := pl.(policy.Engineer); ok { 143 | for n, pn := range eng { 144 | if pn == pl.Name() { 145 | re := e.Engine(n) 146 | if re == nil { 147 | return c.Errf("missing policy engine for plugin %s and name %s", p.Name(), n) 148 | } 149 | p.engines[n] = re 150 | delete(eng, n) 151 | } 152 | } 153 | } 154 | } 155 | 156 | // raise error for engines referenced in the list but not present in the Corefile as plugin 157 | for n := range eng { 158 | return c.Errf("the policy engine %s is missing", n) 159 | } 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /plugin/firewall/setup_test.go: -------------------------------------------------------------------------------- 1 | package firewall 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/coredns/caddy" 7 | ) 8 | 9 | func TestSetup(t *testing.T) { 10 | tests := []struct { 11 | input string 12 | shouldErr bool 13 | queryNbRules int 14 | replyNbRules int 15 | }{ 16 | {`firewall`, true, 0, 0}, 17 | {`firewall {}`, true, 0, 0}, 18 | {`firewall query { 19 | }`, false, 0, 0}, 20 | {`firewall query { 21 | allow true 22 | }`, false, 1, 0}, 23 | {`firewall query { 24 | allow true 25 | } 26 | firewall response { 27 | allow true 28 | }`, false, 1, 1}, 29 | 30 | {`firewall query { 31 | allow true 32 | drop 1 33 | refuse 2+1 34 | block false 35 | }`, false, 4, 0}, 36 | {`firewall query { 37 | allow 38 | }`, true, 1, 0}, 39 | {`firewall query { 40 | allow invalid expression for our / engine 41 | }`, true, 1, 0}, 42 | {`firewall query { 43 | allow true 44 | opa policy parameter 45 | themis whatever parameter to themis 46 | name-of-plugin name-of-policy paramA paramB paramC 47 | }`, false, 4, 0}, 48 | {`firewall query { 49 | allow true 50 | opa policy parameter 51 | themis policy parameter to themis 52 | }`, true, 3, 0}, 53 | {`firewall query { 54 | name-of-plugin-error-if-no-policy-name 55 | }`, true, 1, 0}, 56 | } 57 | for i, test := range tests { 58 | c := caddy.NewTestController("dns", test.input) 59 | fw, err := parse(c) 60 | if test.shouldErr && err == nil { 61 | t.Errorf("Test %v: Expected error but found nil", i) 62 | continue 63 | } else if !test.shouldErr && err != nil { 64 | t.Errorf("Test %v: Expected no error but found error: %v", i, err) 65 | continue 66 | } 67 | if test.shouldErr && err != nil { 68 | continue 69 | } 70 | 71 | if len(fw.query.Rules) != test.queryNbRules { 72 | t.Errorf("Test %v: Expected %v query rules but got %v", i, test.queryNbRules, len(fw.query.Rules)) 73 | continue 74 | } 75 | if len(fw.reply.Rules) != test.replyNbRules { 76 | t.Errorf("Test %v: Expected %v reply rules but got %v", i, test.replyNbRules, len(fw.reply.Rules)) 77 | continue 78 | } 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /plugin/opa/README.md: -------------------------------------------------------------------------------- 1 | # opa 2 | 3 | *opa* - enables OPA to be used as a CoreDNS _firewall_ policy engine. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | opa ENGINE-NAME { 9 | endpoint URL 10 | tls CERT KEY CACERT 11 | fields FIELD [FIELD...] 12 | } 13 | ``` 14 | 15 | * **ENGINE-NAME** is the name of the policy engine, used by the firewall 16 | plugin to uniquely identify the instance. Each instance of _opa_ in 17 | the Corefile must have a unique **ENGINE-NAME**. 18 | 19 | * `endpoint` defines the OPA endpoint **URL**. It should include the 20 | full path to the rule. 21 | 22 | * `tls` **CERT** **KEY** **CACERT** are the TLS cert, key and the CA 23 | cert file names for the OPA connection. 24 | 25 | * `fields` lists the fields that will be sent to OPA when evaluating the 26 | policy for a DNS request/response. Fields available are the same as in 27 | *firewall* plugin expresions: *metadata* from other plugins, and data 28 | from the request/response ("type", "name", "proto", "client_ip", etc). 29 | See the *firewall* README for a list. If this option is omitted, the 30 | following fields are sent: "client_ip", "name", "rcode", "response_ip" 31 | 32 | 33 | ## Firewall Policy Engine 34 | 35 | This plugin is not a standalone plugin. It must be used in conjunction 36 | with the _firewall_ plugin to function. For this plugin to be active, 37 | the _firewall_ plugin must reference it in a rule. See the "Policy 38 | Engine Plugins" section of the _firewall_ plugin README for more 39 | information. 40 | 41 | ## Writing the OPA Policy 42 | 43 | This plugin assumes that the rule referenced in the `endpoint` URL will 44 | evaluate to one of following values: 45 | * "allow" - allows the dns request/response to proceed as normal 46 | * "refuse" - sends a REFUSED response to the client 47 | * "block" - sends a NXDOMAIN response to the client 48 | * "drop" - sends no response to the client 49 | 50 | When writing a rules in OPA, all `fields` are available as input. 51 | 52 | ## Examples 53 | 54 | Point to a local OPA instance using a rule named `action` in the `dns` 55 | package. 56 | 57 | ~~~ txt 58 | . { 59 | opa myengine { 60 | endpoint http://127.0.0.1:8181/v1/data/dns/action 61 | } 62 | 63 | firewall query { 64 | opa myengine 65 | } 66 | } 67 | ~~~ 68 | 69 | The following is an example OPA package. It implements a simple name 70 | blacklist, while whitelisting a client subnet. The rule "action" allows 71 | any request from clients with an IP address in the `1.2.3.0/24` network. 72 | For all other clients it blocks `a.example.com.`, refuses 73 | `b.example.com`, and drops requests for `x.example.com`. It allows all 74 | other requests. 75 | 76 | ~~~ rego 77 | package dns 78 | 79 | import input.name 80 | import input.client_ip 81 | 82 | default action = "allow" 83 | 84 | # Action Priority 85 | action = "allow" { 86 | allow 87 | } else = "refuse" { 88 | refuse 89 | } else = "block" { 90 | block 91 | } else = "drop" { 92 | drop 93 | } 94 | 95 | block { name == "a.example.com." } 96 | 97 | refuse { name == "b.example.com." } 98 | 99 | drop { name == "x.example.com." } 100 | 101 | allow { net.cidr_contains("1.2.3.0/24", client_ip) } 102 | ~~~ -------------------------------------------------------------------------------- /plugin/opa/opa.go: -------------------------------------------------------------------------------- 1 | package opa 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/coredns/coredns/plugin" 12 | "github.com/coredns/coredns/plugin/metadata" 13 | "github.com/coredns/coredns/request" 14 | "github.com/coredns/policy/plugin/firewall/policy" 15 | "github.com/coredns/policy/plugin/pkg/rqdata" 16 | "github.com/miekg/dns" 17 | ) 18 | 19 | // opa is a policy engine plugin for the firewall plugin that can validate DNS requests and 20 | // replies against OPA servers. 21 | type opa struct { 22 | engines map[string]*engine 23 | next plugin.Handler 24 | } 25 | 26 | // engine can validate DNS requests and replies against an OPA server. 27 | type engine struct { 28 | endpoint string // url to opa server api package e.g. http://example.com/v1/data/dns 29 | client *http.Client 30 | fields []string // fields to send as input to opa 31 | mapping *rqdata.Mapping // store this so we dont have to rebuild it for every request 32 | } 33 | 34 | type input map[string]string 35 | 36 | func newOpa() *opa { 37 | return &opa{engines: make(map[string]*engine)} 38 | } 39 | 40 | func newEngine(m *rqdata.Mapping) *engine { 41 | return &engine{ 42 | mapping: m, 43 | fields: []string{"client_ip", "name", "rcode", "response_ip"}, 44 | } 45 | } 46 | 47 | // Name implements the Handler interface 48 | func (p *opa) Name() string { return "opa" } 49 | 50 | // ServeDNS implements the Handler interface 51 | func (p *opa) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { 52 | // do nothing 53 | return plugin.NextOrFailure(p.Name(), p.next, ctx, w, r) 54 | } 55 | 56 | // Engine implements the policy.Engineer interface 57 | func (p *opa) Engine(name string) policy.Engine { 58 | return p.engines[name] 59 | } 60 | 61 | // BuildQueryData implements the policy.Engine interface 62 | func (e *engine) BuildQueryData(ctx context.Context, state request.Request) (interface{}, error) { 63 | return e.buildData(ctx, state, make(input)), nil 64 | } 65 | 66 | // BuildReplyData implements the policy.Engine interface 67 | func (e *engine) BuildReplyData(ctx context.Context, state request.Request, queryData interface{}) (interface{}, error) { 68 | return e.buildData(ctx, state, queryData.(input)), nil 69 | } 70 | 71 | // BuildRule implements the policy.Engine interface 72 | func (e *engine) BuildRule(args []string) (policy.Rule, error) { return e, nil } 73 | 74 | // Evaluate implements the policy.Rule interface 75 | func (e *engine) Evaluate(data interface{}) (int, error) { 76 | // put all query/response data in "input" field, and marshal to json 77 | bdata, err := json.Marshal(map[string]interface{}{"input": data}) 78 | if err != nil { 79 | return 0, err 80 | } 81 | 82 | // send to opa api 83 | resp, err := e.client.Post(e.endpoint, "application/json", bytes.NewBuffer(bdata)) 84 | if err != nil { 85 | return 0, err 86 | } 87 | 88 | // decode response 89 | var result map[string]interface{} 90 | err = json.NewDecoder(resp.Body).Decode(&result) 91 | if err != nil { 92 | return 0, err 93 | } 94 | action, ok := result["result"] 95 | if !ok { 96 | return policy.TypeNone, nil 97 | } 98 | switch action { 99 | case "refuse": 100 | return policy.TypeRefuse, nil 101 | case "allow": 102 | return policy.TypeAllow, nil 103 | case "block": 104 | return policy.TypeBlock, nil 105 | case "drop": 106 | return policy.TypeDrop, nil 107 | default: 108 | return 0, fmt.Errorf("unknown action: '%s'", action) 109 | } 110 | } 111 | 112 | // buildData fills the map of values for policy input 113 | func (e *engine) buildData(ctx context.Context, state request.Request, data input) input { 114 | extractor := rqdata.NewExtractor(state, e.mapping) 115 | for _, f := range e.fields { 116 | if _, ok := data[f]; ok { 117 | // skip if already defined 118 | continue 119 | } 120 | var v string 121 | var ok bool 122 | if e.mapping.ValidField(f) { 123 | v, ok = extractor.Value(f) 124 | if !ok { 125 | continue 126 | } 127 | } else { 128 | mdf := metadata.ValueFunc(ctx, f) 129 | v = mdf() 130 | if v == "" { 131 | continue 132 | } 133 | } 134 | // strip brackets from ipv6 addresses in *_ip fields 135 | if len(v) > 0 && v[0] == '[' && strings.HasSuffix(f, "_ip") { 136 | v = v[1 : len(v)-1] 137 | } 138 | data[f] = v 139 | } 140 | return data 141 | } 142 | -------------------------------------------------------------------------------- /plugin/opa/opa_test.go: -------------------------------------------------------------------------------- 1 | package opa 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/coredns/caddy" 11 | "github.com/coredns/coredns/plugin/test" 12 | "github.com/coredns/coredns/request" 13 | "github.com/coredns/policy/plugin/firewall/policy" 14 | "github.com/coredns/policy/plugin/pkg/response" 15 | "github.com/coredns/policy/plugin/pkg/rqdata" 16 | "github.com/miekg/dns" 17 | ) 18 | 19 | func TestEvaluate(t *testing.T) { 20 | 21 | var apiStub = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | 23 | decoder := json.NewDecoder(r.Body) 24 | var result map[string]map[string]string 25 | err := decoder.Decode(&result) 26 | if err != nil { 27 | w.Write([]byte("{\"result\":\"json decode error\"}")) 28 | } 29 | if _, ok := result["input"]; !ok { 30 | w.Write([]byte("{\"result\":\"request did not contain input\"}")) 31 | } 32 | if result["input"]["a"] != "1" { 33 | w.Write([]byte("{\"result\":\"expected a -> 1\"}")) 34 | } 35 | if result["input"]["b"] != "2" { 36 | w.Write([]byte("{\"result\":\"expected b -> 2\"}")) 37 | } 38 | 39 | w.Write([]byte("{\"result\":\"allow\"}")) 40 | })) 41 | 42 | o, err := parse(caddy.NewTestController("dns", 43 | `opa myengine { 44 | endpoint `+apiStub.URL+` 45 | }`, 46 | )) 47 | 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | data := map[string]string{"a": "1", "b": "2"} 53 | 54 | result, err := o.engines["myengine"].Evaluate(data) 55 | 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | if result != policy.TypeAllow { 61 | t.Errorf("Expected %d, got %d.", policy.TypeAllow, result) 62 | } 63 | } 64 | 65 | func TestBuildQueryData(t *testing.T) { 66 | w := response.NewReader(&test.ResponseWriter{}) 67 | r := new(dns.Msg) 68 | r.SetQuestion("example.org.", dns.TypeA) 69 | state := request.Request{W: w, Req: r} 70 | 71 | e := newEngine(rqdata.NewMapping("")) 72 | ctx := context.TODO() 73 | 74 | d, err := e.BuildQueryData(ctx, state) 75 | if err != nil { 76 | t.Error(err) 77 | } 78 | data := d.(input) 79 | 80 | if data["client_ip"] != "10.240.0.1" { 81 | t.Errorf("expected client_ip == '10.240.0.1'. Got '%v'", data["client_ip"]) 82 | } 83 | if data["name"] != "example.org." { 84 | t.Errorf("expected name == 'example.org.'. Got '%v'", data["name"]) 85 | } 86 | } 87 | 88 | func TestBuildReplyData(t *testing.T) { 89 | r := new(dns.Msg) 90 | r.SetQuestion("example.org.", dns.TypeA) 91 | m := new(dns.Msg) 92 | m.SetReply(r) 93 | m.Rcode = dns.RcodeSuccess 94 | m.Answer = []dns.RR{test.A("example.org. 5 IN A 1.2.3.4")} 95 | 96 | w := &response.Reader{Msg: m} 97 | state := request.Request{W: w, Req: r} 98 | 99 | e := newEngine(rqdata.NewMapping("")) 100 | ctx := context.TODO() 101 | 102 | indata := input{"client_ip": "10.240.0.1", "name": "test.data.exists."} 103 | d, err := e.BuildReplyData(ctx, state, indata) 104 | if err != nil { 105 | t.Error(err) 106 | } 107 | data := d.(input) 108 | 109 | if data["name"] != "test.data.exists." { 110 | t.Errorf("expected name == 'test.data.exists.'. Got '%v'", data["name"]) 111 | } 112 | 113 | if data["rcode"] != "NOERROR" { 114 | t.Errorf("expected rcode == 'NOERROR'. Got '%v'", data["rcode"]) 115 | } 116 | 117 | if data["response_ip"] != "1.2.3.4" { 118 | t.Errorf("expected response_ip == '1.2.3.4'. Got '%v'", data["response_ip"]) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /plugin/opa/setup.go: -------------------------------------------------------------------------------- 1 | package opa 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | 7 | "github.com/coredns/caddy" 8 | "github.com/coredns/coredns/core/dnsserver" 9 | "github.com/coredns/coredns/plugin" 10 | pkgtls "github.com/coredns/coredns/plugin/pkg/tls" 11 | "github.com/coredns/policy/plugin/pkg/rqdata" 12 | ) 13 | 14 | func init() { 15 | caddy.RegisterPlugin("opa", caddy.Plugin{ 16 | ServerType: "dns", 17 | Action: setup, 18 | }) 19 | } 20 | 21 | func setup(c *caddy.Controller) error { 22 | o, err := parse(c) 23 | if err != nil { 24 | return err 25 | } 26 | dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { 27 | o.next = next 28 | return o 29 | }) 30 | return nil 31 | } 32 | 33 | func parse(c *caddy.Controller) (*opa, error) { 34 | o := newOpa() 35 | mapping := rqdata.NewMapping("") 36 | for c.Next() { 37 | args := c.RemainingArgs() 38 | if len(args) != 1 { 39 | return nil, c.ArgErr() 40 | } 41 | name := args[0] 42 | eng := newEngine(mapping) 43 | var tlsConfig *tls.Config 44 | for c.NextBlock() { 45 | switch c.Val() { 46 | case "endpoint": 47 | args := c.RemainingArgs() 48 | if len(args) != 1 { 49 | return nil, c.ArgErr() 50 | } 51 | eng.endpoint = args[0] 52 | case "fields": 53 | args := c.RemainingArgs() 54 | if len(args) == 0 { 55 | return nil, c.ArgErr() 56 | } 57 | // these fields cannot be validated, because metadata fields are not known at setup time 58 | eng.fields = args 59 | case "tls": // cert key cacertfile 60 | args := c.RemainingArgs() 61 | if len(args) == 3 { 62 | var err error 63 | tlsConfig, err = pkgtls.NewTLSConfigFromArgs(args...) 64 | if err != nil { 65 | return nil, err 66 | } 67 | tlsConfig.BuildNameToCertificate() 68 | continue 69 | } 70 | return nil, c.ArgErr() 71 | } 72 | } 73 | if eng.endpoint == "" { 74 | return nil, c.Err("endpoint required") 75 | } 76 | eng.client = &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}} 77 | o.engines[name] = eng 78 | } 79 | return o, nil 80 | } 81 | -------------------------------------------------------------------------------- /plugin/opa/setup_test.go: -------------------------------------------------------------------------------- 1 | package opa 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/coredns/caddy" 7 | ) 8 | 9 | func TestParse(t *testing.T) { 10 | cases := []struct { 11 | input string 12 | expected *opa 13 | shouldErr bool 14 | }{ 15 | {"opa testengine", nil, true}, 16 | 17 | {`opa test engine { 18 | endpoint test 19 | }`, 20 | nil, 21 | true, 22 | }, 23 | 24 | {`opa testengine { 25 | endpoint test 26 | fields 1 2 3 27 | }`, 28 | &opa{engines: map[string]*engine{ 29 | "testengine": {endpoint: "test", fields: []string{"1", "2", "3"}}, 30 | }}, 31 | false, 32 | }, 33 | 34 | {`opa testengine { 35 | endpoint test 36 | fields 1 2 3 37 | } 38 | opa testengine2 { 39 | endpoint test2 40 | fields 4 41 | }`, 42 | &opa{engines: map[string]*engine{ 43 | "testengine": {endpoint: "test", fields: []string{"1", "2", "3"}}, 44 | "testengine2": {endpoint: "test2", fields: []string{"4"}}, 45 | }}, 46 | false, 47 | }, 48 | } 49 | 50 | for i, test := range cases { 51 | c := caddy.NewTestController("dns", test.input) 52 | o, err := parse(c) 53 | 54 | if test.shouldErr && err != nil { 55 | continue 56 | } 57 | 58 | if test.shouldErr && err == nil { 59 | t.Errorf("Test %d: expected error but didn't get one for input %s", i, test.input) 60 | } 61 | 62 | if err != nil { 63 | if !test.shouldErr { 64 | t.Errorf("Test %d: expected no error but got one for input %s, got: %v", i, test.input, err) 65 | } 66 | } 67 | 68 | if o == nil { 69 | t.Errorf("Test %d: got nil result for input %s", i, test.input) 70 | } 71 | 72 | if o.engines == nil { 73 | t.Errorf("Test %d: got nil engines result for input %s", i, test.input) 74 | continue 75 | } 76 | 77 | for name, e := range o.engines { 78 | if e.endpoint != test.expected.engines[name].endpoint { 79 | t.Errorf("Test %d: engine '%s' expected endpoint %s, got %s", i, name, test.expected.engines[name].endpoint, e.endpoint) 80 | } 81 | 82 | if !equal(e.fields, test.expected.engines[name].fields) { 83 | t.Errorf("Test %d: engine '%s' expected fields %v, got %v", i, name, test.expected.engines[name].fields, e.fields) 84 | } 85 | 86 | } 87 | } 88 | 89 | } 90 | 91 | func equal(a, b []string) bool { 92 | if len(a) != len(b) { 93 | return false 94 | } 95 | for i, v := range a { 96 | if v != b[i] { 97 | return false 98 | } 99 | } 100 | return true 101 | } 102 | -------------------------------------------------------------------------------- /plugin/pkg/response/reader.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/miekg/dns" 5 | ) 6 | 7 | // Reader implements ResponseWriter and exposes the message of the response. 8 | type Reader struct { 9 | dns.ResponseWriter 10 | Msg *dns.Msg 11 | } 12 | 13 | // NewReader returns a new Reader 14 | func NewReader(w dns.ResponseWriter) *Reader { 15 | return &Reader{ 16 | ResponseWriter: w, 17 | Msg: nil, 18 | } 19 | } 20 | 21 | // WriteMsg overrides ResponseWriter.WriteMsg 22 | func (r *Reader) WriteMsg(response *dns.Msg) error { 23 | r.Msg = response 24 | return r.ResponseWriter.WriteMsg(response) 25 | } -------------------------------------------------------------------------------- /plugin/pkg/rqdata/rqdata.go: -------------------------------------------------------------------------------- 1 | package rqdata 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/coredns/coredns/request" 9 | "github.com/coredns/policy/plugin/pkg/response" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | type requestFunc func(state request.Request) string 15 | 16 | // Mapping define the mapping between 'name' of data and the way to extract that data from the Request 17 | // it also defines what will be the empty value returned if the data behind the name is empty. 18 | // it is pretty static, and you should need to instantiate only once 19 | type Mapping struct { 20 | replacements map[string]requestFunc 21 | emptyValue string 22 | } 23 | 24 | // Extractor implements a Value(name) (value, valid) function 25 | // which allow to extract data from an existing DNS Request(or state) 26 | type Extractor struct { 27 | state request.Request 28 | requester *Mapping 29 | } 30 | 31 | //NewExtractor return a new Extractor based on the Mapping and the Request provided 32 | func NewExtractor(r request.Request, m *Mapping) *Extractor { 33 | return &Extractor{r, m} 34 | } 35 | 36 | // NewMapping build the mapping name -> func to extract data from the Request 37 | func NewMapping(emptyValue string) *Mapping { 38 | replacements := map[string]requestFunc{ 39 | "type": func(state request.Request) string { 40 | return state.Type() 41 | }, 42 | "name": func(state request.Request) string { 43 | return state.Name() 44 | }, 45 | "class": func(state request.Request) string { 46 | return state.Class() 47 | }, 48 | "proto": func(state request.Request) string { 49 | return state.Proto() 50 | }, 51 | "size": func(state request.Request) string { 52 | return strconv.Itoa(state.Len()) 53 | }, 54 | "client_ip": func(state request.Request) string { 55 | return addrToRFC3986(state.IP()) 56 | }, 57 | "port": func(state request.Request) string { 58 | return addrToRFC3986(state.Port()) 59 | }, 60 | "rcode": func(state request.Request) string { 61 | rcode := "" 62 | rr, ok := state.W.(*response.Reader) 63 | if ok && rr.Msg != nil { 64 | rcode = dns.RcodeToString[rr.Msg.Rcode] 65 | if rcode == "" { 66 | rcode = strconv.Itoa(rr.Msg.Rcode) 67 | } 68 | } 69 | return rcode 70 | }, 71 | "rsize": func(state request.Request) string { 72 | rsize := "" 73 | rr, ok := state.W.(*response.Reader) 74 | if ok && rr.Msg != nil { 75 | rsize = strconv.Itoa(rr.Msg.Len()) 76 | } 77 | return rsize 78 | }, 79 | ">rflags": func(state request.Request) string { 80 | flags := "" 81 | rr, ok := state.W.(*response.Reader) 82 | if ok && rr.Msg != nil { 83 | flags = flagsToString(rr.Msg.MsgHdr) 84 | } 85 | return flags 86 | }, 87 | ">id": func(state request.Request) string { 88 | return strconv.Itoa(int(state.Req.Id)) 89 | }, 90 | ">opcode": func(state request.Request) string { 91 | return strconv.Itoa(int(state.Req.Opcode)) 92 | }, 93 | ">do": func(state request.Request) string { 94 | return boolToString(state.Do()) 95 | }, 96 | ">bufsize": func(state request.Request) string { 97 | return strconv.Itoa(state.Size()) 98 | }, 99 | "server_ip": func(state request.Request) string { 100 | return addrToRFC3986(state.LocalIP()) 101 | }, 102 | "server_port": func(state request.Request) string { 103 | return addrToRFC3986(state.LocalPort()) 104 | }, 105 | "response_ip": func(state request.Request) string { 106 | rr, ok := state.W.(*response.Reader) 107 | if ok && rr.Msg != nil { 108 | ip := respIP(rr.Msg) 109 | if ip != nil { 110 | return addrToRFC3986(ip.String()) 111 | } 112 | } 113 | return "" 114 | }, 115 | } 116 | return &Mapping{replacements, emptyValue} 117 | } 118 | 119 | func (m *Mapping) ValidField(name string) bool { 120 | _, ok := m.replacements[name] 121 | return ok 122 | } 123 | 124 | // Value extract the data that is mapped to this name and return the corresponding value as a string 125 | // if that value is empty then the defaultValue is returned 126 | // Second parameter is a boolean that inform if the name itself is supported in the mapping 127 | func (rd *Extractor) Value(name string) (string, bool) { 128 | f, ok := rd.requester.replacements[name] 129 | if ok { 130 | v := f(rd.state) 131 | if v != "" { 132 | return v, true 133 | } 134 | return rd.requester.emptyValue, true 135 | } 136 | return "", false 137 | } 138 | 139 | func boolToString(b bool) string { 140 | if b { 141 | return "true" 142 | } 143 | return "false" 144 | } 145 | 146 | // flagsToString checks all header flags and returns those 147 | // that are set as a string separated with commas 148 | func flagsToString(h dns.MsgHdr) string { 149 | flags := make([]string, 7) 150 | i := 0 151 | 152 | if h.Response { 153 | flags[i] = "qr" 154 | i++ 155 | } 156 | 157 | if h.Authoritative { 158 | flags[i] = "aa" 159 | i++ 160 | } 161 | if h.Truncated { 162 | flags[i] = "tc" 163 | i++ 164 | } 165 | if h.RecursionDesired { 166 | flags[i] = "rd" 167 | i++ 168 | } 169 | if h.RecursionAvailable { 170 | flags[i] = "ra" 171 | i++ 172 | } 173 | if h.Zero { 174 | flags[i] = "z" 175 | i++ 176 | } 177 | if h.AuthenticatedData { 178 | flags[i] = "ad" 179 | i++ 180 | } 181 | if h.CheckingDisabled { 182 | flags[i] = "cd" 183 | i++ 184 | } 185 | return strings.Join(flags[:i], ",") 186 | } 187 | 188 | // addrToRFC3986 will add brackets to the address if it is an IPv6 address. 189 | func addrToRFC3986(addr string) string { 190 | if strings.Contains(addr, ":") { 191 | return "[" + addr + "]" 192 | } 193 | return addr 194 | } 195 | 196 | // respIP return the first A or AAAA records found in the Answer of the DNS msg 197 | func respIP(r *dns.Msg) net.IP { 198 | if r == nil { 199 | return nil 200 | } 201 | 202 | var ip net.IP 203 | for _, rr := range r.Answer { 204 | switch rr := rr.(type) { 205 | case *dns.A: 206 | ip = rr.A 207 | 208 | case *dns.AAAA: 209 | ip = rr.AAAA 210 | } 211 | // If there are several responses, currently 212 | // only return the first one and break. 213 | if ip != nil { 214 | break 215 | } 216 | } 217 | return ip 218 | } 219 | -------------------------------------------------------------------------------- /plugin/pkg/rqdata/rqdata_test.go: -------------------------------------------------------------------------------- 1 | package rqdata 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/coredns/coredns/plugin/test" 8 | "github.com/coredns/coredns/request" 9 | "github.com/coredns/policy/plugin/pkg/response" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | func buildExtractorOnSimpleMsg(mapping *Mapping) *Extractor { 15 | 16 | w := response.NewReader(&test.ResponseWriter{}) 17 | 18 | r := new(dns.Msg) 19 | r.SetQuestion("example.org.", dns.TypeHINFO) 20 | r.MsgHdr.AuthenticatedData = true 21 | state := request.Request{Req: r, W: w} 22 | 23 | return &Extractor{state, mapping} 24 | } 25 | 26 | func buildExtractorOnRepliedMsg(mapping *Mapping) *Extractor { 27 | w := response.NewReader(&test.ResponseWriter{}) 28 | 29 | r := new(dns.Msg) 30 | r.SetQuestion("example.org.", dns.TypeA) 31 | 32 | ret := new(dns.Msg) 33 | ret.SetReply(r) 34 | ret.Answer = append(ret.Answer, test.A("example.org. IN A 127.0.0.1")) 35 | w.WriteMsg(ret) 36 | state := request.Request{Req: r, W: w} 37 | 38 | return &Extractor{state, mapping} 39 | } 40 | 41 | func TestNewRequestData(t *testing.T) { 42 | 43 | mapping := NewMapping("") 44 | extractFromQuery := buildExtractorOnSimpleMsg(mapping) 45 | extractFromReply := buildExtractorOnRepliedMsg(mapping) 46 | tests := []struct { 47 | extractor *Extractor 48 | name string 49 | value string 50 | subValue string 51 | error bool 52 | }{ 53 | {extractFromQuery, "type", "HINFO", "", false}, 54 | {extractFromQuery, "name", "example.org.", "", false}, 55 | {extractFromQuery, "size", "29", "", false}, 56 | {extractFromQuery, "invalid", "", "", true}, 57 | {extractFromReply, "response_ip", "127.0.0.1", "", false}, 58 | {extractFromReply, "rcode", "NOERROR", "", false}, 59 | } 60 | 61 | for i, tst := range tests { 62 | d, ok := tst.extractor.Value(tst.name) 63 | if !ok { 64 | if !tst.error { 65 | t.Errorf("Test %d, name : %s : unexpected invalid name returned", i, tst.name) 66 | } 67 | continue 68 | } 69 | if tst.error { 70 | t.Errorf("Test %d, name : %s : unexpected valid name returned with value %s", i, tst.name, tst.value) 71 | } 72 | if len(tst.subValue) > 0 { 73 | if !strings.Contains(d, tst.subValue) { 74 | t.Errorf("Test %d, name %s : valued returned : %s, expected to include : %s", i, tst.name, d, tst.subValue) 75 | } 76 | continue 77 | } 78 | if d != tst.value { 79 | t.Errorf("Test %d, name %s : valued returned : %s, expected : %s", i, tst.name, d, tst.value) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /plugin/themis/README.md: -------------------------------------------------------------------------------- 1 | # themis 2 | 3 | *themis* - implements Infoblox's Themis policy engine as CoreDNS _firewall_ policy engine. 4 | 5 | ## Syntax 6 | 7 | ``` 8 | themis ENGINE-NAME { 9 | pdp POLICY-FILE CONTENT [CONTENT...] 10 | endpoint PDP [PDP...] 11 | attr NAME LABEL [DSTTYPE] 12 | debug_query_suffix SUFFIX 13 | debug_id ID 14 | metrics METRIC [METRIC...] 15 | streams COUNT [BALANCE] 16 | connection_timeout 17 | transfer ATTR [ATTR...] 18 | dnstap 19 | connection_timeout 20 | log 21 | max_request_size [[auto] SIZE] 22 | max_response_attributes auto | COUNT 23 | cache [TTL [SIZE]] 24 | } 25 | ``` 26 | 27 | * **ENGINE-NAME** is the name of the policy engine, used by the firewall plugin to uniquely identify the instance. 28 | Each instance of _themis_ in the Corefile must have a unique **ENGINE-NAME**. 29 | 30 | * `pdp` defines themis policy and content files for local policy evaluation 31 | 32 | * `endpoint` defines a list themis **PDP** addresses for remote policy evaluation 33 | 34 | * `attr` is used for assigning labels into PDP attributes. `attr` may be defined multiple times. 35 | **DSTTYPE** allowed values depends on Themis PDP implementation, e.g. string (default), domain, address. 36 | 37 | * `debug_query_suffix` enables debug query feature. **SUFFIX** must end with a dot. 38 | 39 | * `debug_id` is used to assist debugging. **ID** is a unique id that can be used to help determine 40 | which CoreDNS instance created a response. 41 | 42 | * `metrics` defines a list of prometheus metrics to report. 43 | 44 | * `streams` **COUNT** sets the number of gRPC streams for PDP connections. 45 | **BALANCE** can be `round-robin` (default), or `hot-spot`. 46 | 47 | * `transfer` defines the set of attributes from domain validation response tha 48 | should be inserted into IP validation request. 49 | 50 | * `dnstap` defines attributes to be included in the extra field of DNStap message if received 51 | from the PDP. 52 | 53 | * `connection_timeout` sets the timeout for query validation when no PDP servers are available. 54 | A negative value or `no` means wait forever, the default behavior. A timeout of `0` causes 55 | validation to fail instantly if there are no PDP servers. The option works only if gRPC streams are 56 | greater than 0. 57 | 58 | * `log` enables logging of the PDP request and response 59 | 60 | * `max_request_size` sets maximum buffer size in bytes for serialized request. Setting the limit 61 | too high will make the plugin to allocate too much memory. Setting the limit too small can lead 62 | to buffer overflow errors during validation. If `auto` is set, the plugin allocates the required 63 | amount of bytes for each request. If both `auto` and **SIZE** are set, **SIZE** is only used for 64 | cache allocations and not for limiting the request buffer. 65 | 66 | * `max_response_attributes` sets the maximum number of attributes expected from the PDP. If the value 67 | is `auto`, the plugin automatically allocates the necessary number of attributes for each PDP response. 68 | 69 | * `cache` enables decision cache. **TTL** default value is 10 minutes. **SIZE** limits the memory cache 70 | will use to given number of megabytes. If **SIZE** isn't provided cache can grow indeterminately. 71 | 72 | ## Firewall Policy Engine 73 | 74 | This plugin is not a standalone plugin. It must be used in conjunction with the _firewall_ plugin to function. 75 | For this plugin to be active, the _firewall_ plugin must reference it in a rule. See the "Policy Engine Plugins" 76 | section of the _firewall_ plugin README for more information. 77 | 78 | ## Examples 79 | 80 | In the Corefile below, edns0 options with code 0xffee is split into two values - client_id (first 16 bytes) 81 | and group_id (last 16 bytes). Edns0 options less than 32 bytes in size will not assign a client_id or group_id. 82 | 83 | ~~~ txt 84 | . { 85 | themis myengine { 86 | endpoint 10.0.0.7:1234 10.0.0.8:1234 87 | attr client_id request/client_id 88 | attr group_id request/group_id 89 | attr uid request/another_id 90 | attr source_ip request/source_ip address 91 | attr client_name request/client_name 92 | debug_query_suffix debug. 93 | debug_id instance_1 94 | streams 100 95 | transfer gid uid 96 | log 97 | } 98 | 99 | firewall query { 100 | themis myengine 101 | } 102 | } 103 | ~~~ 104 | 105 | -------------------------------------------------------------------------------- /plugin/themis/attrholder.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "strconv" 8 | 9 | "context" 10 | "strings" 11 | 12 | "github.com/coredns/coredns/plugin/metadata" 13 | "github.com/coredns/policy/plugin/firewall/policy" 14 | rq "github.com/coredns/policy/plugin/pkg/rqdata" 15 | "github.com/infobloxopen/go-trees/domain" 16 | "github.com/infobloxopen/themis/pdp" 17 | "github.com/miekg/dns" 18 | ) 19 | 20 | var emptyCtx *pdp.Context 21 | 22 | type attrSetting struct { 23 | name string 24 | label string 25 | attrType string 26 | metrics bool 27 | } 28 | 29 | type attrHolder struct { 30 | dn string 31 | 32 | dnReq []pdp.AttributeAssignment 33 | dnRes []pdp.AttributeAssignment 34 | 35 | transfer []pdp.AttributeAssignment 36 | 37 | ipReq []pdp.AttributeAssignment 38 | ipRes []pdp.AttributeAssignment 39 | 40 | dnstap []pdp.AttributeAssignment 41 | 42 | action byte 43 | dst string 44 | } 45 | 46 | func init() { 47 | emptyCtx, _ = pdp.NewContext(nil, 0, nil) 48 | } 49 | 50 | func setAttrRequestValueMetadata(label string) { 51 | 52 | } 53 | 54 | func newAttrHolderWithContext(ctx context.Context, xtr *rq.Extractor, optMap []*attrSetting, ag *AttrGauge) *attrHolder { 55 | 56 | hdrCount := ednsAttrsStart 57 | qName, _ := xtr.Value("name") 58 | qType, _ := xtr.Value("type") 59 | dn, err := domain.MakeNameFromString(qName) 60 | if err != nil { 61 | panic(fmt.Errorf("Can't treat %q as domain name: %s", qName, err)) 62 | } 63 | 64 | clientIP, _ := xtr.Value("client_ip") 65 | var srcIP = net.IP(nil) 66 | if clientIP != "" { 67 | srcIP = net.ParseIP(clientIP) 68 | } 69 | if srcIP != nil { 70 | hdrCount++ 71 | } 72 | 73 | ah := &attrHolder{ 74 | dn: qName, 75 | dnReq: make([]pdp.AttributeAssignment, hdrCount, 8), 76 | } 77 | 78 | ah.dnReq[0] = pdp.MakeStringAssignment(attrNameType, typeValueQuery) 79 | ah.dnReq[1] = pdp.MakeDomainAssignment(attrNameDomainName, dn) 80 | ah.dnReq[2] = pdp.MakeStringAssignment(attrNameDNSQtype, strconv.FormatUint(uint64(dns.StringToType[qType]), 16)) 81 | 82 | if srcIP != nil { 83 | ah.dnReq[3] = pdp.MakeAddressAssignment(attrNameSourceIP, srcIP) 84 | } 85 | 86 | for _, o := range optMap { 87 | f := metadata.ValueFunc(ctx, o.label) 88 | if f == nil { 89 | continue 90 | } 91 | value := f() 92 | if value == "" { 93 | continue 94 | } 95 | if a, ok := makeAssignmentByType(o, value); ok { 96 | if o.name == attrNameSourceIP && srcIP != nil { 97 | ah.dnReq[3] = a 98 | } else { 99 | ah.dnReq = append(ah.dnReq, a) 100 | if o.metrics && ag != nil { 101 | ag.Inc(a) 102 | } 103 | } 104 | } 105 | } 106 | 107 | return ah 108 | } 109 | 110 | func makeAssignmentByType(o *attrSetting, value string) (pdp.AttributeAssignment, bool) { 111 | switch strings.ToLower(o.attrType) { 112 | case "address": 113 | return pdp.MakeAddressAssignment(o.name, net.ParseIP(value)), true 114 | case "string": 115 | return pdp.MakeStringAssignment(o.name, value), true 116 | } 117 | panic(fmt.Errorf("unknown attribute type %s", o.attrType)) 118 | } 119 | 120 | func (ah *attrHolder) addDnRes(r *pdp.Response, custAttrs map[string]custAttr) { 121 | oCount := len(r.Obligations) 122 | 123 | switch r.Effect { 124 | default: 125 | log.Printf("[ERROR] PDP Effect: %s, Reason: %s", pdp.EffectNameFromEnum(r.Effect), r.Status) 126 | ah.action = policy.TypeNone 127 | 128 | case pdp.EffectPermit: 129 | ah.action = policy.TypeAllow 130 | 131 | i := 0 132 | for i < oCount { 133 | o := r.Obligations[i] 134 | 135 | id := o.GetID() 136 | switch id { 137 | case attrNameLog: 138 | //ah.action = policy.TypeLog 139 | 140 | default: 141 | if t, ok := custAttrs[id]; ok { 142 | ah.putCustomAttr(o, t) 143 | 144 | if t.isEdns() { 145 | oCount-- 146 | r.Obligations[i] = r.Obligations[oCount] 147 | continue 148 | } 149 | } 150 | } 151 | 152 | i++ 153 | } 154 | 155 | case pdp.EffectDeny: 156 | ah.action = policy.TypeBlock 157 | 158 | i := 0 159 | for i < oCount { 160 | o := r.Obligations[i] 161 | 162 | id := o.GetID() 163 | switch id { 164 | default: 165 | if t, ok := custAttrs[id]; ok && t.isEdns() { 166 | ah.putCustomAttr(o, t) 167 | 168 | oCount-- 169 | r.Obligations[i] = r.Obligations[oCount] 170 | continue 171 | } 172 | 173 | case attrNameRefuse: 174 | ah.action = policy.TypeRefuse 175 | 176 | case attrNameDrop: 177 | ah.action = policy.TypeDrop 178 | } 179 | 180 | i++ 181 | } 182 | } 183 | 184 | ah.dnRes = r.Obligations[:oCount] 185 | } 186 | 187 | func (ah *attrHolder) putCustomAttr(attr pdp.AttributeAssignment, f custAttr) { 188 | if f.isEdns() { 189 | id := attr.GetID() 190 | 191 | for _, a := range ah.dnReq[ednsAttrsStart:] { 192 | if id == a.GetID() { 193 | return 194 | } 195 | } 196 | 197 | ah.dnReq = append(ah.dnReq, attr) 198 | } 199 | 200 | if f.isTransfer() { 201 | ah.transfer = append(ah.transfer, attr) 202 | } 203 | 204 | if f.isDnstap() { 205 | ah.dnstap = append(ah.dnstap, attr) 206 | } 207 | } 208 | 209 | func (ah *attrHolder) prepareResponseFromContext(ctx context.Context, xtr *rq.Extractor) { 210 | ipResp, _ := xtr.Value("response_ip") 211 | if ipResp != "" { 212 | ip := net.ParseIP(ipResp) 213 | if ip != nil { 214 | ah.addIPReq(ip) 215 | } 216 | } 217 | } 218 | 219 | func (ah *attrHolder) addIPReq(ip net.IP) { 220 | ah.ipReq = append( 221 | []pdp.AttributeAssignment{ 222 | pdp.MakeStringAssignment(attrNameType, typeValueResponse), 223 | pdp.MakeAddressAssignment(attrNameAddress, ip), 224 | }, 225 | ah.transfer..., 226 | ) 227 | } 228 | 229 | func (ah *attrHolder) addIPRes(r *pdp.Response) { 230 | switch r.Effect { 231 | default: 232 | log.Printf("[ERROR] PDP Effect: %s, Reason: %s", pdp.EffectNameFromEnum(r.Effect), r.Status) 233 | ah.action = policy.TypeNone 234 | 235 | case pdp.EffectPermit: 236 | ah.action = policy.TypeAllow 237 | 238 | //for _, o := range r.Obligations { 239 | // if o.GetID() == attrNameLog { 240 | // ah.action = firewall.TypeLog 241 | // break 242 | // } 243 | //} 244 | 245 | case pdp.EffectDeny: 246 | ah.action = policy.TypeBlock 247 | 248 | for _, o := range r.Obligations { 249 | switch o.GetID() { 250 | case attrNameRefuse: 251 | ah.action = policy.TypeRefuse 252 | 253 | //case attrNameRedirectTo: 254 | // ah.addRedirect(o) 255 | 256 | case attrNameDrop: 257 | ah.action = policy.TypeDrop 258 | } 259 | } 260 | } 261 | 262 | ah.ipRes = r.Obligations 263 | } 264 | -------------------------------------------------------------------------------- /plugin/themis/attrholder_test.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "fmt" 5 | "github.com/coredns/policy/plugin/firewall/policy" 6 | "github.com/coredns/policy/plugin/pkg/rqdata" 7 | "net" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/coredns/coredns/request" 12 | "github.com/infobloxopen/go-trees/domain" 13 | "github.com/miekg/dns" 14 | 15 | "context" 16 | 17 | "github.com/coredns/coredns/plugin/metadata" 18 | "github.com/infobloxopen/themis/pdp" 19 | ) 20 | 21 | type fakeWriter struct { 22 | dns.ResponseWriter 23 | clientIP string 24 | serverIP string 25 | } 26 | 27 | func (w *fakeWriter) LocalAddr() net.Addr { 28 | ip := net.ParseIP(w.serverIP) 29 | return &net.UDPAddr{IP: ip, Port: 0} // Port is not used here 30 | } 31 | 32 | func (w *fakeWriter) RemoteAddr() net.Addr { 33 | ip := net.ParseIP(w.clientIP) 34 | return &net.UDPAddr{IP: ip, Port: 0} // Port is not used here 35 | } 36 | 37 | func buildFunc(v string) metadata.Func { 38 | return func() string { return v } 39 | } 40 | func buildContext(ctx context.Context, data map[string]string) context.Context { 41 | ctx = metadata.ContextWithMetadata(ctx) 42 | for k, v := range data { 43 | metadata.SetValueFunc(ctx, k, buildFunc(v)) 44 | } 45 | return ctx 46 | } 47 | 48 | func buildState(name string, qtype uint16, clientIp string) request.Request { 49 | //create a msg 50 | req := new(dns.Msg) 51 | req.SetQuestion(name, qtype) 52 | return request.Request{Req: req, W: &fakeWriter{clientIP: clientIp}} 53 | } 54 | 55 | func TestNewAttrHolderWithDnReq(t *testing.T) { 56 | optsMap := []*attrSetting{ 57 | {"low", "request/low", "String", false}, 58 | {"high", "request/high", "String", false}, 59 | {"byte", "request/byte", "String", false}, 60 | {attrNameSourceIP, "client_ip", "Address", false}, 61 | } 62 | 63 | mapping := rqdata.NewMapping("") 64 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 65 | mdata := map[string]string{ 66 | "request/low": "0001020304050607", 67 | "request/high": "08090a0b0c0d0e0f", 68 | "request/byte": "test", 69 | } 70 | 71 | ctx := buildContext(context.TODO(), mdata) 72 | 73 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), optsMap, nil) 74 | pdp.AssertAttributeAssignments(t, "newAttrHolderWithDnReq", ah.dnReq, 75 | pdp.MakeStringAssignment(attrNameType, typeValueQuery), 76 | pdp.MakeDomainAssignment(attrNameDomainName, makeTestDomain(dns.Fqdn("example.com"))), 77 | pdp.MakeStringAssignment(attrNameDNSQtype, strconv.FormatUint(uint64(dns.TypeA), 16)), 78 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.1")), 79 | pdp.MakeStringAssignment("low", "0001020304050607"), 80 | pdp.MakeStringAssignment("high", "08090a0b0c0d0e0f"), 81 | pdp.MakeStringAssignment("byte", "test"), 82 | ) 83 | 84 | state = buildState("...", dns.TypeA, "example.com:53") 85 | ctx = buildContext(context.TODO(), mdata) 86 | assertPanicWithError(t, "newAttrHolderWithDnReq(invalidDomainName)", func() { 87 | newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), optsMap, nil) 88 | }, "Can't treat %q as domain name: %s", "...", domain.ErrEmptyLabel) 89 | } 90 | 91 | func TestAddIpReq(t *testing.T) { 92 | 93 | optsMap := []*attrSetting{ 94 | {attrNameSourceIP, "request/source_ip", "Address", false}, 95 | } 96 | mdata := map[string]string{} 97 | mapping := rqdata.NewMapping("") 98 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 99 | 100 | ctx := buildContext(context.TODO(), mdata) 101 | 102 | custAttrs := map[string]custAttr{ 103 | "trans": custAttrTransfer, 104 | } 105 | 106 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), optsMap, nil) 107 | pdp.AssertAttributeAssignments(t, "newAttrHolderWithDnReq - dnReq", ah.dnReq, 108 | pdp.MakeStringAssignment(attrNameType, typeValueQuery), 109 | pdp.MakeDomainAssignment(attrNameDomainName, makeTestDomain(dns.Fqdn("example.com"))), 110 | pdp.MakeStringAssignment(attrNameDNSQtype, strconv.FormatUint(uint64(dns.TypeA), 16)), 111 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.1")), 112 | ) 113 | pdp.AssertAttributeAssignments(t, "newAttrHolderWithDnReq - dnRes", ah.dnRes) 114 | 115 | ah.addDnRes( 116 | &pdp.Response{ 117 | Effect: pdp.EffectPermit, 118 | Obligations: []pdp.AttributeAssignment{ 119 | pdp.MakeStringAssignment("trans", "val"), 120 | }, 121 | }, 122 | custAttrs, 123 | ) 124 | pdp.AssertAttributeAssignments(t, "addDnRes - dnRes", ah.dnRes, 125 | pdp.MakeStringAssignment("trans", "val"), 126 | ) 127 | pdp.AssertAttributeAssignments(t, "addDnRes - transfer", ah.transfer, 128 | pdp.MakeStringAssignment("trans", "val"), 129 | ) 130 | 131 | ah.addIPReq(net.ParseIP("2001:db8::1")) 132 | pdp.AssertAttributeAssignments(t, "addIPReq - ipReq", ah.ipReq, 133 | pdp.MakeStringAssignment(attrNameType, typeValueResponse), 134 | pdp.MakeAddressAssignment(attrNameAddress, net.ParseIP("2001:db8::1")), 135 | pdp.MakeStringAssignment("trans", "val"), 136 | ) 137 | } 138 | 139 | func TestActionDomainResponse(t *testing.T) { 140 | tests := []struct { 141 | res *pdp.Response 142 | action byte 143 | dst string 144 | }{ 145 | { 146 | res: &pdp.Response{ 147 | Effect: pdp.EffectPermit, 148 | }, 149 | action: policy.TypeAllow, 150 | }, 151 | { 152 | res: &pdp.Response{ 153 | Effect: pdp.EffectIndeterminate, 154 | Status: fmt.Errorf("example of pdp failure on domain validation"), 155 | }, 156 | action: policy.TypeNone, 157 | }, 158 | { 159 | res: &pdp.Response{ 160 | Effect: pdp.EffectDeny, 161 | }, 162 | action: policy.TypeBlock, 163 | }, 164 | //{ 165 | // res: &pdp.Response{ 166 | // Effect: pdp.EffectDeny, 167 | // Obligations: []pdp.AttributeAssignment{ 168 | // pdp.MakeStringAssignment(attrNameRedirectTo, "192.0.2.1"), 169 | // }, 170 | // }, 171 | // action: firewall.TypeRedirect, 172 | // dst: "192.0.2.1", 173 | //}, 174 | /* attrNameRedirectTo is not currently implemented AFAICT 175 | { 176 | res: &pdp.Response{ 177 | Effect: pdp.EffectDeny, 178 | Obligations: []pdp.AttributeAssignment{ 179 | pdp.MakeIntegerAssignment(attrNameRedirectTo, 0), 180 | }, 181 | }, 182 | action: policy.TypeNone, 183 | }, 184 | */ 185 | { 186 | res: &pdp.Response{ 187 | Effect: pdp.EffectDeny, 188 | Obligations: []pdp.AttributeAssignment{ 189 | pdp.MakeBooleanAssignment(attrNameRefuse, true), 190 | }, 191 | }, 192 | action: policy.TypeRefuse, 193 | }, 194 | //{ 195 | // res: &pdp.Response{ 196 | // Effect: pdp.EffectPermit, 197 | // Obligations: []pdp.AttributeAssignment{ 198 | // pdp.MakeBooleanAssignment(attrNameLog, true), 199 | // }, 200 | // }, 201 | // action: firewall.TypeLog, 202 | //}, 203 | { 204 | res: &pdp.Response{ 205 | Effect: pdp.EffectDeny, 206 | Obligations: []pdp.AttributeAssignment{ 207 | pdp.MakeBooleanAssignment(attrNameDrop, true), 208 | }, 209 | }, 210 | action: policy.TypeDrop, 211 | }, 212 | } 213 | 214 | mdata := map[string]string{} 215 | mapping := rqdata.NewMapping("") 216 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 217 | 218 | for i, test := range tests { 219 | t.Run(strconv.Itoa(i), func(t *testing.T) { 220 | optMap := make([]*attrSetting, 0) 221 | ctx := buildContext(context.TODO(), mdata) 222 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), optMap, nil) 223 | 224 | g := newLogGrabber() 225 | ah.addDnRes(test.res, nil) 226 | logs := g.Release() 227 | 228 | if ah.action != test.action { 229 | t.Errorf("unexpected action in TC #%d: expected=%d, actual=%d", i, test.action, ah.action) 230 | t.Logf("=== plugin logs ===\n%s--- plugin logs ---", logs) 231 | } 232 | if ah.dst != test.dst { 233 | t.Errorf("unexpected redirect destination in TC #%d: expected=%q, actual=%q", i, test.dst, ah.dst) 234 | t.Logf("=== plugin logs ===\n%s--- plugin logs ---", logs) 235 | } 236 | }) 237 | } 238 | } 239 | 240 | func TestActionIpResponse(t *testing.T) { 241 | tests := []struct { 242 | res *pdp.Response 243 | initAction byte 244 | action byte 245 | dst string 246 | }{ 247 | { 248 | res: &pdp.Response{ 249 | Effect: pdp.EffectPermit, 250 | }, 251 | initAction: policy.TypeAllow, 252 | action: policy.TypeAllow, 253 | }, 254 | //{ 255 | // res: &pdp.Response{ 256 | // Effect: pdp.EffectPermit, 257 | // }, 258 | // initAction: policy.TypeLog, 259 | // action: policy.TypeAllow, 260 | //}, 261 | //{ 262 | // res: &pdp.Response{ 263 | // Effect: pdp.EffectPermit, 264 | // Obligations: []pdp.AttributeAssignment{ 265 | // pdp.MakeBooleanAssignment(attrNameLog, true), 266 | // }, 267 | // }, 268 | // initAction: policy.TypeAllow, 269 | // action: policy.TypeLog, 270 | //}, 271 | { 272 | res: &pdp.Response{ 273 | Effect: pdp.EffectDeny, 274 | }, 275 | initAction: policy.TypeAllow, 276 | action: policy.TypeBlock, 277 | }, 278 | //{ 279 | // res: &pdp.Response{ 280 | // Effect: pdp.EffectDeny, 281 | // Obligations: []pdp.AttributeAssignment{ 282 | // pdp.MakeStringAssignment(attrNameRedirectTo, "192.0.2.1"), 283 | // }, 284 | // }, 285 | // initAction: policy.TypeAllow, 286 | // action: policy.TypeRedirect, 287 | // dst: "192.0.2.1", 288 | //}, 289 | { 290 | res: &pdp.Response{ 291 | Effect: pdp.EffectDeny, 292 | Obligations: []pdp.AttributeAssignment{ 293 | pdp.MakeBooleanAssignment(attrNameRefuse, true), 294 | }, 295 | }, 296 | initAction: policy.TypeAllow, 297 | action: policy.TypeRefuse, 298 | }, 299 | { 300 | res: &pdp.Response{ 301 | Effect: pdp.EffectDeny, 302 | Obligations: []pdp.AttributeAssignment{ 303 | pdp.MakeBooleanAssignment(attrNameDrop, true), 304 | }, 305 | }, 306 | initAction: policy.TypeAllow, 307 | action: policy.TypeDrop, 308 | }, 309 | { 310 | res: &pdp.Response{ 311 | Effect: pdp.EffectIndeterminate, 312 | Status: fmt.Errorf("example of pdp failure on IP validation"), 313 | }, 314 | initAction: policy.TypeAllow, 315 | action: policy.TypeNone, 316 | }, 317 | } 318 | mdata := map[string]string{} 319 | mapping := rqdata.NewMapping("") 320 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 321 | 322 | for i, test := range tests { 323 | t.Run(strconv.Itoa(i), func(t *testing.T) { 324 | optMap := make([]*attrSetting, 0) 325 | ctx := buildContext(context.TODO(), mdata) 326 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), optMap, nil) 327 | 328 | ah.action = test.initAction 329 | 330 | g := newLogGrabber() 331 | ah.addIPRes(test.res) 332 | logs := g.Release() 333 | 334 | if ah.action != test.action { 335 | t.Errorf("unexpected action in TC #%d: expected=%d, actual=%d", i, test.action, ah.action) 336 | t.Logf("=== plugin logs ===\n%s--- plugin logs ---", logs) 337 | } 338 | if ah.dst != test.dst { 339 | t.Errorf("unexpected redirect destination in TC #%d: expected=%q, actual=%q", i, test.dst, ah.dst) 340 | t.Logf("=== plugin logs ===\n%s--- plugin logs ---", logs) 341 | } 342 | }) 343 | } 344 | } 345 | 346 | func TestAddResponse(t *testing.T) { 347 | mdata := map[string]string{ 348 | "request/edns1": "edns1Val", 349 | } 350 | mapping := rqdata.NewMapping("") 351 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 352 | 353 | optsMap := []*attrSetting{ 354 | {"edns1", "request/edns1", "String", false}, 355 | } 356 | 357 | custAttrs := map[string]custAttr{ 358 | "edns1": custAttrEdns, 359 | "trans1": custAttrTransfer, 360 | "transdnstap": custAttrTransfer | custAttrDnstap, 361 | "dnstap": custAttrDnstap, 362 | } 363 | 364 | tests := []struct { 365 | opts []*attrSetting 366 | resp *pdp.Response 367 | dnRes []pdp.AttributeAssignment 368 | ipRes []pdp.AttributeAssignment 369 | edns0 []pdp.AttributeAssignment 370 | trans []pdp.AttributeAssignment 371 | dnstap []pdp.AttributeAssignment 372 | }{ 373 | { 374 | resp: &pdp.Response{ 375 | Effect: pdp.EffectPermit, 376 | }, 377 | dnRes: []pdp.AttributeAssignment{}, 378 | ipRes: []pdp.AttributeAssignment{}, 379 | edns0: []pdp.AttributeAssignment{ 380 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.1")), 381 | }, 382 | trans: []pdp.AttributeAssignment{}, 383 | dnstap: []pdp.AttributeAssignment{}, 384 | }, 385 | { 386 | opts: optsMap, 387 | resp: &pdp.Response{ 388 | Effect: pdp.EffectPermit, 389 | }, 390 | dnRes: []pdp.AttributeAssignment{}, 391 | ipRes: []pdp.AttributeAssignment{}, 392 | edns0: []pdp.AttributeAssignment{ 393 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.2")), 394 | pdp.MakeStringAssignment("edns1", "edns1Val"), 395 | }, 396 | trans: []pdp.AttributeAssignment{}, 397 | }, 398 | { 399 | resp: &pdp.Response{ 400 | Effect: pdp.EffectPermit, 401 | Obligations: []pdp.AttributeAssignment{ 402 | pdp.MakeStringAssignment("edns1", "edns1Val"), 403 | }, 404 | }, 405 | dnRes: []pdp.AttributeAssignment{}, 406 | edns0: []pdp.AttributeAssignment{ 407 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.3")), 408 | pdp.MakeStringAssignment("edns1", "edns1Val"), 409 | }, 410 | trans: []pdp.AttributeAssignment{}, 411 | }, 412 | { 413 | resp: &pdp.Response{ 414 | Effect: pdp.EffectDeny, 415 | Obligations: []pdp.AttributeAssignment{ 416 | pdp.MakeStringAssignment("edns1", "edns1Val"), 417 | }, 418 | }, 419 | dnRes: []pdp.AttributeAssignment{}, 420 | edns0: []pdp.AttributeAssignment{ 421 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.4")), 422 | pdp.MakeStringAssignment("edns1", "edns1Val"), 423 | }, 424 | trans: []pdp.AttributeAssignment{}, 425 | }, 426 | { 427 | resp: &pdp.Response{ 428 | Effect: pdp.EffectPermit, 429 | Obligations: []pdp.AttributeAssignment{ 430 | pdp.MakeStringAssignment("edns1", "edns1Val"), 431 | }, 432 | }, 433 | ipRes: []pdp.AttributeAssignment{ 434 | pdp.MakeStringAssignment("edns1", "edns1Val"), 435 | }, 436 | edns0: []pdp.AttributeAssignment{ 437 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.5")), 438 | }, 439 | trans: []pdp.AttributeAssignment{}, 440 | }, 441 | { 442 | opts: optsMap, 443 | resp: &pdp.Response{ 444 | Effect: pdp.EffectPermit, 445 | Obligations: []pdp.AttributeAssignment{ 446 | pdp.MakeStringAssignment("edns1", "edns1Val2"), 447 | }, 448 | }, 449 | dnRes: []pdp.AttributeAssignment{}, 450 | edns0: []pdp.AttributeAssignment{ 451 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.6")), 452 | pdp.MakeStringAssignment("edns1", "edns1Val"), 453 | }, 454 | trans: []pdp.AttributeAssignment{}, 455 | }, 456 | { 457 | resp: &pdp.Response{ 458 | Effect: pdp.EffectPermit, 459 | Obligations: []pdp.AttributeAssignment{ 460 | pdp.MakeStringAssignment("edns1", "edns1Val2"), 461 | }, 462 | }, 463 | dnRes: []pdp.AttributeAssignment{}, 464 | edns0: []pdp.AttributeAssignment{ 465 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.7")), 466 | pdp.MakeStringAssignment("edns1", "edns1Val2"), 467 | }, 468 | trans: []pdp.AttributeAssignment{}, 469 | }, 470 | { 471 | resp: &pdp.Response{ 472 | Effect: pdp.EffectPermit, 473 | Obligations: []pdp.AttributeAssignment{ 474 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 475 | }, 476 | }, 477 | ipRes: []pdp.AttributeAssignment{ 478 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 479 | }, 480 | edns0: []pdp.AttributeAssignment{ 481 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.8")), 482 | }, 483 | trans: []pdp.AttributeAssignment{}, 484 | }, 485 | { 486 | resp: &pdp.Response{ 487 | Effect: pdp.EffectPermit, 488 | Obligations: []pdp.AttributeAssignment{ 489 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 490 | }, 491 | }, 492 | dnRes: []pdp.AttributeAssignment{ 493 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 494 | }, 495 | edns0: []pdp.AttributeAssignment{ 496 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.9")), 497 | }, 498 | trans: []pdp.AttributeAssignment{ 499 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 500 | }, 501 | }, 502 | { 503 | resp: &pdp.Response{ 504 | Effect: pdp.EffectDeny, 505 | Obligations: []pdp.AttributeAssignment{ 506 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 507 | }, 508 | }, 509 | dnRes: []pdp.AttributeAssignment{ 510 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 511 | }, 512 | edns0: []pdp.AttributeAssignment{ 513 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.10")), 514 | }, 515 | trans: []pdp.AttributeAssignment{}, 516 | }, 517 | { 518 | resp: &pdp.Response{ 519 | Effect: pdp.EffectPermit, 520 | Obligations: []pdp.AttributeAssignment{ 521 | pdp.MakeStringAssignment("edns1", "ends1Val"), 522 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 523 | pdp.MakeStringAssignment("other1", "other1Val1"), 524 | }, 525 | }, 526 | dnRes: []pdp.AttributeAssignment{ 527 | pdp.MakeStringAssignment("other1", "other1Val1"), 528 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 529 | }, 530 | edns0: []pdp.AttributeAssignment{ 531 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.11")), 532 | pdp.MakeStringAssignment("edns1", "ends1Val"), 533 | }, 534 | trans: []pdp.AttributeAssignment{ 535 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 536 | }, 537 | }, 538 | { 539 | resp: &pdp.Response{ 540 | Effect: pdp.EffectPermit, 541 | Obligations: []pdp.AttributeAssignment{ 542 | pdp.MakeStringAssignment("edns1", "ends1Val"), 543 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 544 | pdp.MakeStringAssignment("other1", "other1Val1"), 545 | }, 546 | }, 547 | ipRes: []pdp.AttributeAssignment{ 548 | pdp.MakeStringAssignment("edns1", "ends1Val"), 549 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 550 | pdp.MakeStringAssignment("other1", "other1Val1"), 551 | }, 552 | edns0: []pdp.AttributeAssignment{ 553 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.12")), 554 | }, 555 | trans: []pdp.AttributeAssignment{}, 556 | }, 557 | { 558 | resp: &pdp.Response{ 559 | Effect: pdp.EffectPermit, 560 | Obligations: []pdp.AttributeAssignment{ 561 | pdp.MakeStringAssignment("edns1", "ends1Val"), 562 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 563 | pdp.MakeStringAssignment("other1", "other1Val1"), 564 | pdp.MakeStringAssignment("transdnstap", "val"), 565 | }, 566 | }, 567 | dnRes: []pdp.AttributeAssignment{ 568 | pdp.MakeStringAssignment("transdnstap", "val"), 569 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 570 | pdp.MakeStringAssignment("other1", "other1Val1"), 571 | }, 572 | edns0: []pdp.AttributeAssignment{ 573 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.13")), 574 | pdp.MakeStringAssignment("edns1", "ends1Val"), 575 | }, 576 | trans: []pdp.AttributeAssignment{ 577 | pdp.MakeStringAssignment("transdnstap", "val"), 578 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 579 | }, 580 | dnstap: []pdp.AttributeAssignment{ 581 | pdp.MakeStringAssignment("transdnstap", "val"), 582 | }, 583 | }, 584 | { 585 | resp: &pdp.Response{ 586 | Effect: pdp.EffectPermit, 587 | Obligations: []pdp.AttributeAssignment{ 588 | pdp.MakeStringAssignment("edns1", "ends1Val"), 589 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 590 | pdp.MakeStringAssignment("other1", "other1Val1"), 591 | pdp.MakeStringAssignment("transdnstap", "val"), 592 | }, 593 | }, 594 | ipRes: []pdp.AttributeAssignment{ 595 | pdp.MakeStringAssignment("edns1", "ends1Val"), 596 | pdp.MakeStringAssignment("trans1", "trans1Val1"), 597 | pdp.MakeStringAssignment("other1", "other1Val1"), 598 | pdp.MakeStringAssignment("transdnstap", "val"), 599 | }, 600 | edns0: []pdp.AttributeAssignment{ 601 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.14")), 602 | }, 603 | trans: []pdp.AttributeAssignment{}, 604 | dnstap: []pdp.AttributeAssignment{}, 605 | }, 606 | { 607 | resp: &pdp.Response{ 608 | Effect: pdp.EffectPermit, 609 | Obligations: []pdp.AttributeAssignment{ 610 | pdp.MakeStringAssignment("other1", "other1Val1"), 611 | pdp.MakeStringAssignment("dnstap", "val"), 612 | }, 613 | }, 614 | dnRes: []pdp.AttributeAssignment{ 615 | pdp.MakeStringAssignment("other1", "other1Val1"), 616 | pdp.MakeStringAssignment("dnstap", "val"), 617 | }, 618 | edns0: []pdp.AttributeAssignment{ 619 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.15")), 620 | }, 621 | trans: []pdp.AttributeAssignment{}, 622 | dnstap: []pdp.AttributeAssignment{ 623 | pdp.MakeStringAssignment("dnstap", "val"), 624 | }, 625 | }, 626 | } 627 | 628 | for i, test := range tests { 629 | t.Run(strconv.Itoa(i), func(t *testing.T) { 630 | state = buildState("example.com.", dns.TypeA, fmt.Sprintf("192.0.2.%d", i+1)) 631 | if test.dnRes != nil { 632 | 633 | optMap := make([]*attrSetting, 0) 634 | if test.opts != nil { 635 | optMap = test.opts 636 | } 637 | ctx := buildContext(context.TODO(), mdata) 638 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), optMap, nil) 639 | 640 | ah.addDnRes(test.resp, custAttrs) 641 | 642 | pdp.AssertAttributeAssignments(t, 643 | fmt.Sprintf("TestAddResponse test %d dnRes", i+1), 644 | ah.dnRes, test.dnRes..., 645 | ) 646 | if test.edns0 != nil { 647 | pdp.AssertAttributeAssignments(t, 648 | fmt.Sprintf("TestAddResponse test %d (dnRes) - edns0", i+1), 649 | ah.dnReq[ednsAttrsStart:], test.edns0..., 650 | ) 651 | } 652 | if test.trans != nil { 653 | pdp.AssertAttributeAssignments(t, 654 | fmt.Sprintf("TestAddResponse test %d (dnRes) - trans", i+1), 655 | ah.transfer, test.trans..., 656 | ) 657 | } 658 | if test.dnstap != nil { 659 | pdp.AssertAttributeAssignments(t, 660 | fmt.Sprintf("TestAddResponse test %d (dnRes) - dnstap", i+1), 661 | ah.dnstap, test.dnstap..., 662 | ) 663 | } 664 | } 665 | 666 | if test.ipRes != nil { 667 | optMap := make([]*attrSetting, 0) 668 | if test.opts != nil { 669 | optMap = test.opts 670 | } 671 | ctx := buildContext(context.TODO(), mdata) 672 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), optMap, nil) 673 | ah.addIPRes(test.resp) 674 | 675 | pdp.AssertAttributeAssignments(t, 676 | fmt.Sprintf("TestAddResponse test %d ipRes", i+1), 677 | ah.dnRes, test.dnRes..., 678 | ) 679 | if test.edns0 != nil { 680 | pdp.AssertAttributeAssignments(t, 681 | fmt.Sprintf("TestAddResponse test %d (ipRes) - edns0", i+1), 682 | ah.dnReq[ednsAttrsStart:], test.edns0..., 683 | ) 684 | } 685 | if test.trans != nil { 686 | pdp.AssertAttributeAssignments(t, 687 | fmt.Sprintf("TestAddResponse test %d (ipRes) - trans", i+1), 688 | ah.transfer, test.trans..., 689 | ) 690 | } 691 | if test.dnstap != nil { 692 | pdp.AssertAttributeAssignments(t, 693 | fmt.Sprintf("TestAddResponse test %d (ipRes) - dnstap", i+1), 694 | ah.dnstap, test.dnstap..., 695 | ) 696 | } 697 | } 698 | }) 699 | } 700 | } 701 | 702 | func TestMakeDnstapReport(t *testing.T) { 703 | 704 | mdata := map[string]string{} 705 | mapping := rqdata.NewMapping("") 706 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 707 | 708 | optsMap := []*attrSetting{} 709 | 710 | custAttrs := map[string]custAttr{ 711 | "trans": custAttrTransfer, 712 | "dnstap": custAttrDnstap, 713 | } 714 | 715 | ctx := buildContext(context.TODO(), mdata) 716 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), optsMap, nil) 717 | pdp.AssertAttributeAssignments(t, "newAttrHolderWithDnReq - dnReq", ah.dnReq, 718 | pdp.MakeStringAssignment(attrNameType, typeValueQuery), 719 | pdp.MakeDomainAssignment(attrNameDomainName, makeTestDomain(dns.Fqdn("example.com"))), 720 | pdp.MakeStringAssignment(attrNameDNSQtype, strconv.FormatUint(uint64(dns.TypeA), 16)), 721 | pdp.MakeAddressAssignment(attrNameSourceIP, net.ParseIP("192.0.2.1")), 722 | ) 723 | pdp.AssertAttributeAssignments(t, "newAttrHolderWithDnReq - dnRes", ah.dnRes) 724 | 725 | ah.addDnRes( 726 | &pdp.Response{ 727 | Effect: pdp.EffectPermit, 728 | Obligations: []pdp.AttributeAssignment{ 729 | pdp.MakeStringAssignment("trans", "transVal"), 730 | pdp.MakeStringAssignment("dnstap", "dnstapVal"), 731 | }, 732 | }, 733 | custAttrs, 734 | ) 735 | pdp.AssertAttributeAssignments(t, "addDnRes - dnRes", ah.dnRes, 736 | pdp.MakeStringAssignment("trans", "transVal"), 737 | pdp.MakeStringAssignment("dnstap", "dnstapVal"), 738 | ) 739 | pdp.AssertAttributeAssignments(t, "addDnRes - transfer", ah.transfer, 740 | pdp.MakeStringAssignment("trans", "transVal"), 741 | ) 742 | pdp.AssertAttributeAssignments(t, "addDnRes - dnstap", ah.dnstap, 743 | pdp.MakeStringAssignment("dnstap", "dnstapVal"), 744 | ) 745 | 746 | ah.addIPReq(net.ParseIP("2001:db8::1")) 747 | pdp.AssertAttributeAssignments(t, "addIPReq - ipReq", ah.ipReq, 748 | pdp.MakeStringAssignment(attrNameType, typeValueResponse), 749 | pdp.MakeAddressAssignment(attrNameAddress, net.ParseIP("2001:db8::1")), 750 | pdp.MakeStringAssignment("trans", "transVal"), 751 | ) 752 | 753 | ah.addIPRes( 754 | &pdp.Response{ 755 | Effect: pdp.EffectPermit, 756 | Obligations: []pdp.AttributeAssignment{ 757 | pdp.MakeStringAssignment("other", "otherVal"), 758 | }, 759 | }, 760 | ) 761 | 762 | pdp.AssertAttributeAssignments(t, "addIPRes - dnRes", ah.dnRes, 763 | pdp.MakeStringAssignment("trans", "transVal"), 764 | pdp.MakeStringAssignment("dnstap", "dnstapVal"), 765 | ) 766 | pdp.AssertAttributeAssignments(t, "addIPRes - ipRes", ah.ipRes, 767 | pdp.MakeStringAssignment("other", "otherVal"), 768 | ) 769 | pdp.AssertAttributeAssignments(t, "addIPRes - transfer", ah.transfer, 770 | pdp.MakeStringAssignment("trans", "transVal"), 771 | ) 772 | pdp.AssertAttributeAssignments(t, "addIPRes - dnstap", ah.dnstap, 773 | pdp.MakeStringAssignment("dnstap", "dnstapVal"), 774 | ) 775 | 776 | } 777 | 778 | func makeTestDomain(s string) domain.Name { 779 | dn, err := domain.MakeNameFromString(s) 780 | if err != nil { 781 | panic(err) 782 | } 783 | 784 | return dn 785 | } 786 | 787 | func assertPanicWithError(t *testing.T, desc string, f func(), format string, args ...interface{}) { 788 | defer func() { 789 | if r := recover(); r != nil { 790 | e := fmt.Sprintf(format, args...) 791 | err, ok := r.(error) 792 | if !ok { 793 | t.Errorf("excpected error %q on panic for %q but got %T (%#v)", e, desc, r, r) 794 | } else if err.Error() != e { 795 | t.Errorf("excpected error %q on panic for %q but got %q", e, desc, r) 796 | } 797 | } else { 798 | t.Errorf("expected panic %q for %q", fmt.Sprintf(format, args...), desc) 799 | } 800 | }() 801 | 802 | f() 803 | } 804 | -------------------------------------------------------------------------------- /plugin/themis/attributes.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import "github.com/infobloxopen/themis/pdp" 4 | 5 | const ( 6 | attrNameType = "type" 7 | attrNameDomainName = "domain_name" 8 | attrNameDNSQtype = "dns_qtype" 9 | attrNameSourceIP = "source_ip" 10 | attrNameAddress = "address" 11 | attrNameLog = "log" 12 | attrNameRedirectTo = "redirect_to" 13 | attrNameRefuse = "refuse" 14 | attrNameDrop = "drop" 15 | attrNamePolicyAction = "policy_action" 16 | 17 | typeValueQuery = "query" 18 | typeValueResponse = "response" 19 | 20 | ednsAttrsStart = 3 21 | ipReqAddrPos = 1 22 | ) 23 | 24 | func serializeOrPanic(a pdp.AttributeAssignment) string { 25 | v, err := a.GetValue() 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | s, err := v.Serialize() 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | return s 36 | } 37 | 38 | type custAttr byte 39 | 40 | const ( 41 | custAttrEdns = 1 << iota 42 | custAttrTransfer 43 | custAttrDnstap 44 | custAttrMetrics 45 | ) 46 | 47 | func (a custAttr) isEdns() bool { 48 | return a&custAttrEdns != 0 49 | } 50 | 51 | func (a custAttr) isTransfer() bool { 52 | return a&custAttrTransfer != 0 53 | } 54 | 55 | func (a custAttr) isDnstap() bool { 56 | return a&custAttrDnstap != 0 57 | } 58 | 59 | func (a custAttr) isMetrics() bool { 60 | return a&custAttrMetrics != 0 61 | } 62 | -------------------------------------------------------------------------------- /plugin/themis/attributes_test.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/infobloxopen/themis/pdp" 8 | ) 9 | 10 | func TestSerializeOrPanic(t *testing.T) { 11 | s := serializeOrPanic(pdp.MakeStringAssignment("s", "test")) 12 | if s != "test" { 13 | t.Errorf("expected %q but got %q", "test", s) 14 | } 15 | 16 | assertPanicWithErrorContains(t, "serializeOrPanic(expression)", func() { 17 | serializeOrPanic(pdp.MakeExpressionAssignment("s", pdp.MakeStringDesignator("s"))) 18 | }, "pdp.AttributeDesignator") 19 | 20 | assertPanicWithErrorContains(t, "serializeOrPanic(undefined)", func() { 21 | serializeOrPanic(pdp.MakeExpressionAssignment("s", pdp.UndefinedValue)) 22 | }, "Undefined") 23 | } 24 | 25 | func TestCustAttr(t *testing.T) { 26 | if !custAttr(custAttrEdns).isEdns() { 27 | t.Errorf("expected %d is EDNS", custAttrEdns) 28 | } 29 | 30 | if !custAttr(custAttrTransfer).isTransfer() { 31 | t.Errorf("expected %d is transfer", custAttrTransfer) 32 | } 33 | 34 | if !custAttr(custAttrDnstap).isDnstap() { 35 | t.Errorf("expected %d is DNStap", custAttrDnstap) 36 | } 37 | } 38 | 39 | func assertPanicWithErrorContains(t *testing.T, desc string, f func(), e string) { 40 | defer func() { 41 | if r := recover(); r != nil { 42 | err, ok := r.(error) 43 | if !ok { 44 | t.Errorf("excpected error containing %q on panic for %q but got %T (%#v)", e, desc, r, r) 45 | } else if !strings.Contains(err.Error(), e) { 46 | t.Errorf("excpected error containing %q on panic for %q but got %q", e, desc, r) 47 | } 48 | } else { 49 | t.Errorf("expected error containing %q on panic for %q", e, desc) 50 | } 51 | }() 52 | 53 | f() 54 | } 55 | -------------------------------------------------------------------------------- /plugin/themis/client.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "github.com/coredns/policy/plugin/themis/client" 5 | "log" 6 | "sync/atomic" 7 | 8 | //"github.com/coredns/coredns/plugin/pkg/trace" 9 | "github.com/infobloxopen/themis/pdp" 10 | "github.com/infobloxopen/themis/pep" 11 | ) 12 | 13 | // connect establishes connection to PDP server. 14 | func (p *ThemisEngine) connect() error { 15 | log.Printf("[DEBUG] Connecting %v", p) 16 | 17 | for _, addr := range p.conf.endpoints { 18 | p.connAttempts[addr] = new(uint32) 19 | } 20 | 21 | opts := []pep.Option{ 22 | pep.WithConnectionTimeout(p.conf.connTimeout), 23 | pep.WithConnectionStateNotification(p.connStateCb), 24 | } 25 | 26 | if p.conf.cacheTTL > 0 { 27 | if p.conf.cacheLimit > 0 { 28 | opts = append(opts, pep.WithCacheTTLAndMaxSize(p.conf.cacheTTL, p.conf.cacheLimit)) 29 | } else { 30 | opts = append(opts, pep.WithCacheTTL(p.conf.cacheTTL)) 31 | } 32 | } 33 | 34 | if p.conf.streams <= 0 || !p.conf.hotSpot { 35 | opts = append(opts, pep.WithRoundRobinBalancer(p.conf.endpoints...)) 36 | } 37 | 38 | if p.conf.streams > 0 { 39 | opts = append(opts, pep.WithStreams(p.conf.streams)) 40 | if p.conf.hotSpot { 41 | opts = append(opts, pep.WithHotSpotBalancer(p.conf.endpoints...)) 42 | } 43 | } 44 | 45 | opts = append(opts, pep.WithAutoRequestSize(p.conf.autoReqSize)) 46 | if p.conf.maxReqSize > 0 { 47 | opts = append(opts, pep.WithMaxRequestSize(uint32(p.conf.maxReqSize))) 48 | } 49 | 50 | p.attrPool = makeAttrPool(p.conf.maxResAttrs, false) 51 | 52 | //if p.trace != nil { 53 | // if t, ok := p.trace.(trace.Trace); ok { 54 | // opts = append(opts, pep.WithTracer(t.Tracer())) 55 | // } 56 | //} 57 | 58 | if p.conf.policyFile != "" { 59 | p.pdp = client.NewBuiltinClient(p.conf.policyFile, p.conf.contentFiles) 60 | } else { 61 | p.pdp = pep.NewClient(opts...) 62 | } 63 | 64 | return p.pdp.Connect("") 65 | } 66 | 67 | // closeConn terminates previously established connection. 68 | func (p *ThemisEngine) closeConn() { 69 | if p.pdp != nil { 70 | go func() { 71 | p.wg.Wait() 72 | p.pdp.Close() 73 | }() 74 | } 75 | } 76 | 77 | func (p *ThemisEngine) validate(ah *attrHolder, a []pdp.AttributeAssignment) error { 78 | var req []pdp.AttributeAssignment 79 | if len(ah.ipReq) > 0 { 80 | req = ah.ipReq 81 | } else { 82 | req = ah.dnReq 83 | } 84 | 85 | if p.conf.log { 86 | log.Printf("[INFO] PDP request: %+v", req) 87 | } 88 | 89 | res := pdp.Response{Obligations: a} 90 | err := p.pdp.Validate(req, &res) 91 | if err != nil { 92 | log.Printf("[ERROR] Policy validation failed due to error %s", err) 93 | return err 94 | } 95 | 96 | if p.conf.log { 97 | log.Printf("[INFO] PDP response: %+v", res) 98 | } 99 | 100 | if len(ah.ipReq) > 0 { 101 | ah.addIPRes(&res) 102 | } else { 103 | ah.addDnRes(&res, p.conf.custAttrs) 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func (p *ThemisEngine) connStateCb(addr string, state int, err error) { 110 | switch state { 111 | default: 112 | if err != nil { 113 | log.Printf("[DEBUG] Unknown connection notification %s (%s)", addr, err) 114 | } else { 115 | log.Printf("[DEBUG] Unknown connection notification %s", addr) 116 | } 117 | 118 | case pep.StreamingConnectionEstablished: 119 | ptr, ok := p.connAttempts[addr] 120 | if !ok { 121 | ptr = p.unkConnAttempts 122 | } 123 | atomic.StoreUint32(ptr, 0) 124 | 125 | log.Printf("[INFO] Connected to %s", addr) 126 | 127 | case pep.StreamingConnectionBroken: 128 | log.Printf("[ERROR] Connection to %s has been broken", addr) 129 | 130 | case pep.StreamingConnectionConnecting: 131 | ptr, ok := p.connAttempts[addr] 132 | if !ok { 133 | ptr = p.unkConnAttempts 134 | } 135 | count := atomic.AddUint32(ptr, 1) 136 | 137 | if count <= 1 { 138 | log.Printf("[INFO] Connecting to %s", addr) 139 | } 140 | 141 | if count > 100 { 142 | log.Printf("[ERROR] Connecting to %s", addr) 143 | atomic.StoreUint32(ptr, 1) 144 | } 145 | 146 | case pep.StreamingConnectionFailure: 147 | ptr, ok := p.connAttempts[addr] 148 | if !ok { 149 | ptr = p.unkConnAttempts 150 | } 151 | if atomic.LoadUint32(ptr) <= 1 { 152 | log.Printf("[ERROR] Failed to connect to %s (%s)", addr, err) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /plugin/themis/client/builtin_client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/infobloxopen/themis/pdp" 10 | "github.com/infobloxopen/themis/pdp/ast" 11 | "github.com/infobloxopen/themis/pdp/jcon" 12 | // the following is for initializing pdp local selector 13 | _ "github.com/infobloxopen/themis/pdp/selector" 14 | ) 15 | 16 | type builtinClient struct { 17 | policyFile string 18 | contentFiles []string 19 | 20 | parser ast.Parser 21 | 22 | p *pdp.PolicyStorage 23 | c *pdp.LocalContentStorage 24 | } 25 | 26 | func NewBuiltinClient(policyFile string, contentFiles []string) *builtinClient { 27 | c := &builtinClient{ 28 | policyFile: policyFile, 29 | contentFiles: contentFiles, 30 | } 31 | return c 32 | } 33 | 34 | func (c *builtinClient) setPolicyParser() { 35 | if c.policyFile != "" { 36 | ext := filepath.Ext(c.policyFile) 37 | switch ext { 38 | case ".json": 39 | c.parser = ast.NewJSONParser() 40 | case ".yaml": 41 | c.parser = ast.NewYAMLParser() 42 | } 43 | } 44 | } 45 | 46 | func (c *builtinClient) loadPolicies() error { 47 | log.Printf("[INFO] Loading policy '%s'", c.policyFile) 48 | c.setPolicyParser() 49 | 50 | pf, err := os.Open(c.policyFile) 51 | if err != nil { 52 | log.Printf("[ERROR] Failed to open policy file: %s", err) 53 | return err 54 | } 55 | 56 | log.Printf("[INFO] Parsing policy '%s'", c.policyFile) 57 | p, err := c.parser.Unmarshal(pf, nil) 58 | if err != nil { 59 | log.Printf("[ERROR] Failed to parse policy: %s", err) 60 | return err 61 | } 62 | 63 | c.p = p 64 | return nil 65 | } 66 | 67 | func (c *builtinClient) loadContents() error { 68 | log.Print("[INFO] Loading content") 69 | 70 | var items []*pdp.LocalContent 71 | for _, path := range c.contentFiles { 72 | err := func() error { 73 | log.Printf("[INFO] Opening content '%s'", path) 74 | f, err := os.Open(path) 75 | if err != nil { 76 | log.Printf("[ERROR] Failed to open content: %s", err) 77 | return err 78 | } 79 | 80 | defer f.Close() 81 | 82 | log.Printf("[INFO] Parsing content '%s'", path) 83 | item, err := jcon.Unmarshal(f, nil) 84 | if err != nil { 85 | log.Printf("[ERROR] Failed to parse content: %s", err) 86 | return err 87 | } 88 | 89 | items = append(items, item) 90 | return nil 91 | }() 92 | if err != nil { 93 | return err 94 | } 95 | } 96 | 97 | c.c = pdp.NewLocalContentStorage(items) 98 | return nil 99 | } 100 | 101 | func (c *builtinClient) Connect(addr string) error { 102 | if err := c.loadPolicies(); err != nil { 103 | return err 104 | } 105 | 106 | if err := c.loadContents(); err != nil { 107 | return err 108 | } 109 | 110 | return nil 111 | } 112 | 113 | func (c *builtinClient) Close() { 114 | c.p = nil 115 | c.c = nil 116 | } 117 | 118 | func (c *builtinClient) Validate(in, out interface{}) error { 119 | req, ok := in.([]pdp.AttributeAssignment) 120 | if !ok { 121 | return fmt.Errorf("unknown request type passed to Validate()") 122 | } 123 | res, ok := out.(*pdp.Response) 124 | if !ok { 125 | return fmt.Errorf("unknown response type passed to Validate()") 126 | } 127 | 128 | ctx, err := pdp.NewContext(c.c, len(req), func(i int) (string, pdp.AttributeValue, error) { 129 | id := req[i].GetID() 130 | v, err := req[i].GetValue() 131 | return id, v, err 132 | }) 133 | if err != nil { 134 | return fmt.Errorf("error creating pdp context '%s'", err) 135 | } 136 | 137 | r := c.p.Root().Calculate(ctx) 138 | 139 | res.Effect = r.Effect 140 | res.Status = r.Status 141 | if res.Obligations == nil { 142 | res.Obligations = r.Obligations 143 | } else { 144 | obLen := len(r.Obligations) 145 | if obLen > len(res.Obligations) { 146 | return fmt.Errorf("result obligations too small %d < %d", 147 | len(res.Obligations), obLen) 148 | } 149 | 150 | copy(res.Obligations, r.Obligations) 151 | res.Obligations = res.Obligations[:obLen] 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /plugin/themis/client_test.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/coredns/policy/plugin/firewall/policy" 7 | "github.com/coredns/policy/plugin/pkg/rqdata" 8 | "github.com/miekg/dns" 9 | "net" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "context" 15 | 16 | "github.com/infobloxopen/themis/pdp" 17 | _ "github.com/infobloxopen/themis/pdp/selector" 18 | "github.com/infobloxopen/themis/pdpserver/server" 19 | ) 20 | 21 | const testPolicy = `# Policy set for client interaction tests 22 | attributes: 23 | type: string 24 | domain_name: domain 25 | address: address 26 | rule: string 27 | log: string 28 | o1: string 29 | o2: string 30 | o3: string 31 | 32 | policies: 33 | alg: FirstApplicableEffect 34 | policies: 35 | - id: "Query Policy" 36 | target: 37 | - equal: 38 | - attr: type 39 | - val: 40 | type: string 41 | content: query 42 | alg: FirstApplicableEffect 43 | rules: 44 | - id: "Query for example.com" 45 | target: 46 | - contains: 47 | - val: 48 | type: set of domains 49 | content: 50 | - example.com 51 | - attr: domain_name 52 | effect: Permit 53 | obligations: 54 | - rule: 55 | val: 56 | type: string 57 | content: "Query rule for example.com" 58 | - id: "Many obligations rule" 59 | target: 60 | - contains: 61 | - val: 62 | type: set of domains 63 | content: 64 | - overflow.me 65 | - attr: domain_name 66 | effect: Permit 67 | obligations: 68 | - rule: 69 | val: 70 | type: string 71 | content: "Many obligations rule" 72 | - o1: 73 | val: 74 | type: string 75 | content: "First additional obligation" 76 | - o2: 77 | val: 78 | type: string 79 | content: "Second additional obligation" 80 | - o3: 81 | val: 82 | type: string 83 | content: "Third additional obligation" 84 | - id: "Response Policy" 85 | target: 86 | - equal: 87 | - attr: type 88 | - val: 89 | type: string 90 | content: response 91 | alg: FirstApplicableEffect 92 | rules: 93 | - id: "Response for 192.0.2.0/28" 94 | target: 95 | - contains: 96 | - val: 97 | type: set of networks 98 | content: 99 | - 192.0.2.0/28 100 | - attr: address 101 | effect: Permit 102 | obligations: 103 | - rule: 104 | val: 105 | type: string 106 | content: "Response rule for 192.0.2.0/28" 107 | - log: 108 | val: 109 | type: string 110 | content: "" 111 | ` 112 | 113 | type logGrabber struct { 114 | b *bytes.Buffer 115 | } 116 | 117 | func newLogGrabber() *logGrabber { 118 | b := new(bytes.Buffer) 119 | //log.SetOutput(b) 120 | 121 | return &logGrabber{ 122 | b: b, 123 | } 124 | } 125 | 126 | func (g *logGrabber) Release() string { 127 | //log.SetOutput(os.Stderr) 128 | 129 | return g.b.String() 130 | } 131 | 132 | func TestStreamingClientInteraction(t *testing.T) { 133 | endpoint := "127.0.0.1:5555" 134 | srv := startPDPServer(t, testPolicy, endpoint) 135 | defer func() { 136 | if logs := srv.Stop(); len(logs) > 0 { 137 | t.Logf("server logs:\n%s", logs) 138 | } 139 | }() 140 | 141 | if err := waitForPortOpened(endpoint); err != nil { 142 | t.Fatalf("can't connect to PDP server: %s", err) 143 | } 144 | 145 | g := newLogGrabber() 146 | ok := t.Run("noCache", func(t *testing.T) { 147 | p := newThemisEngine() 148 | p.conf.endpoints = []string{endpoint} 149 | p.conf.connTimeout = time.Second 150 | p.conf.streams = 1 151 | p.conf.log = true 152 | 153 | if err := p.connect(); err != nil { 154 | t.Fatal(err) 155 | } 156 | defer p.closeConn() 157 | 158 | mdata := map[string]string{} 159 | mapping := rqdata.NewMapping("") 160 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 161 | 162 | ctx := buildContext(context.TODO(), mdata) 163 | 164 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), p.conf.options, nil) 165 | 166 | attrs := make([]pdp.AttributeAssignment, p.conf.maxResAttrs) 167 | if err := p.validate(ah, attrs); err != nil { 168 | t.Error(err) 169 | } 170 | 171 | if ah.action != policy.TypeAllow { 172 | aName := fmt.Sprintf("unknown action %d", ah.action) 173 | if ah.action >= 0 && int(ah.action) < len(policy.NameTypes) { 174 | aName = policy.NameTypes[int(ah.action)] 175 | } 176 | t.Errorf("expected %q action but got %q", policy.NameTypes[policy.TypeAllow], aName) 177 | } 178 | 179 | pdp.AssertAttributeAssignments(t, "p.validate(domain request)", ah.dnRes, 180 | pdp.MakeStringAssignment("rule", "Query rule for example.com"), 181 | ) 182 | 183 | ah.addIPReq(net.ParseIP("192.0.2.1")) 184 | 185 | attrs = make([]pdp.AttributeAssignment, p.conf.maxResAttrs) 186 | if err := p.validate(ah, attrs); err != nil { 187 | t.Error(err) 188 | } 189 | 190 | //if ah.action != policy.TypeLog { 191 | // aName := fmt.Sprintf("unknown action %d", ah.action) 192 | // if ah.action >= 0 && int(ah.action) < len(policy.NameTypes) { 193 | // aName = policy.NameTypes[int(ah.action)] 194 | // } 195 | // t.Errorf("expected %q action but got %q", policy.NameTypes[policy.TypeLog], aName) 196 | //} 197 | 198 | pdp.AssertAttributeAssignments(t, "p.validate(domain request)", ah.ipRes, 199 | pdp.MakeStringAssignment("rule", "Response rule for 192.0.2.0/28"), 200 | pdp.MakeStringAssignment("log", ""), 201 | ) 202 | }) 203 | 204 | logs := g.Release() 205 | if !ok { 206 | t.Logf("=== plugin logs ===\n%s--- plugin logs ---", logs) 207 | } 208 | 209 | g = newLogGrabber() 210 | ok = t.Run("cacheTTL", func(t *testing.T) { 211 | p := newThemisEngine() 212 | p.conf.endpoints = []string{endpoint} 213 | p.conf.connTimeout = time.Second 214 | p.conf.streams = 1 215 | p.conf.log = true 216 | p.conf.maxReqSize = 128 217 | p.conf.cacheTTL = 10 * time.Minute 218 | 219 | if err := p.connect(); err != nil { 220 | t.Fatal(err) 221 | } 222 | defer p.closeConn() 223 | 224 | mdata := map[string]string{} 225 | mapping := rqdata.NewMapping("") 226 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 227 | 228 | ctx := buildContext(context.TODO(), mdata) 229 | 230 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping), p.conf.options, nil) 231 | 232 | attrs := make([]pdp.AttributeAssignment, p.conf.maxResAttrs) 233 | if err := p.validate(ah, attrs); err != nil { 234 | t.Error(err) 235 | } 236 | 237 | if ah.action != policy.TypeAllow { 238 | aName := fmt.Sprintf("unknown action %d", ah.action) 239 | if ah.action >= 0 && int(ah.action) < len(policy.NameTypes) { 240 | aName = policy.NameTypes[int(ah.action)] 241 | } 242 | t.Errorf("expected %q action but got %q", policy.NameTypes[policy.TypeAllow], aName) 243 | } 244 | 245 | pdp.AssertAttributeAssignments(t, "p.validate(domain request)", ah.dnRes, 246 | pdp.MakeStringAssignment("rule", "Query rule for example.com"), 247 | ) 248 | }) 249 | 250 | logs = g.Release() 251 | if !ok { 252 | t.Logf("=== plugin logs ===\n%s--- plugin logs ---", logs) 253 | } 254 | 255 | g = newLogGrabber() 256 | ok = t.Run("cacheTTLAndLimit", func(t *testing.T) { 257 | p := newThemisEngine() 258 | p.conf.endpoints = []string{endpoint} 259 | p.conf.connTimeout = time.Second 260 | p.conf.streams = 1 261 | p.conf.log = true 262 | p.conf.maxReqSize = 128 263 | p.conf.cacheTTL = 10 * time.Minute 264 | p.conf.cacheLimit = 128 265 | 266 | if err := p.connect(); err != nil { 267 | t.Fatal(err) 268 | } 269 | defer p.closeConn() 270 | 271 | mdata := map[string]string{} 272 | mapping := rqdata.NewMapping("") 273 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 274 | 275 | ctx := buildContext(context.TODO(), mdata) 276 | 277 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping),p.conf.options, nil) 278 | attrs := make([]pdp.AttributeAssignment, p.conf.maxResAttrs) 279 | if err := p.validate(ah, attrs); err != nil { 280 | t.Error(err) 281 | } 282 | 283 | if ah.action != policy.TypeAllow { 284 | aName := fmt.Sprintf("unknown action %d", ah.action) 285 | if ah.action >= 0 && int(ah.action) < len(policy.NameTypes) { 286 | aName = policy.NameTypes[int(ah.action)] 287 | } 288 | t.Errorf("expected %q action but got %q", policy.NameTypes[policy.TypeAllow], aName) 289 | } 290 | 291 | pdp.AssertAttributeAssignments(t, "p.validate(domain request)", ah.dnRes, 292 | pdp.MakeStringAssignment("rule", "Query rule for example.com"), 293 | ) 294 | }) 295 | 296 | logs = g.Release() 297 | if !ok { 298 | t.Logf("=== plugin logs ===\n%s--- plugin logs ---", logs) 299 | } 300 | } 301 | 302 | // AFAICT, the following test fails because the upstream themis streaming client (not in this project) 303 | // does not error on overflow. IMO, this test should be upstream, not here. 304 | /* 305 | func TestStreamingClientInteractionWithObligationsOverflow(t *testing.T) { 306 | endpoint := "127.0.0.1:5555" 307 | srv := startPDPServer(t, testPolicy, endpoint) 308 | defer func() { 309 | if logs := srv.Stop(); len(logs) > 0 { 310 | t.Logf("server logs:\n%s", logs) 311 | } 312 | }() 313 | 314 | if err := waitForPortOpened(endpoint); err != nil { 315 | t.Fatalf("can't connect to PDP server: %s", err) 316 | } 317 | 318 | ok := true 319 | g := newLogGrabber() 320 | defer func() { 321 | logs := g.Release() 322 | if !ok { 323 | t.Logf("=== plugin logs ===\n%s--- plugin logs ---", logs) 324 | } 325 | }() 326 | 327 | p := newThemisEngine() 328 | p.conf.endpoints = []string{endpoint} 329 | p.conf.connTimeout = time.Second 330 | p.conf.streams = 1 331 | p.conf.maxResAttrs = 3 332 | p.conf.log = true 333 | 334 | if err := p.connect(); err != nil { 335 | t.Fatal(err) 336 | ok = false 337 | } 338 | defer p.closeConn() 339 | 340 | mdata := map[string]string{} 341 | mapping := rqdata.NewMapping("") 342 | state := buildState("example.com.", dns.TypeA, "192.0.2.1") 343 | 344 | ctx := buildContext(context.TODO(), mdata) 345 | 346 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, mapping),p.conf.options, nil) 347 | attrs := make([]pdp.AttributeAssignment, p.conf.maxResAttrs) 348 | for i := range attrs { 349 | attrs[i] = pdp.MakeStringAssignment("blah", "blah") 350 | } 351 | err := p.validate(ah, attrs) 352 | if err == nil { 353 | aName := fmt.Sprintf("unknown action %d", ah.action) 354 | if ah.action >= 0 && int(ah.action) < len(policy.NameTypes) { 355 | aName = policy.NameTypes[int(ah.action)] 356 | } 357 | 358 | t.Errorf("expected response overflow error but got %q response:\n:%+v", aName, ah.dnRes) 359 | ok = false 360 | } 361 | } 362 | */ 363 | 364 | func startPDPServer(t *testing.T, p, endpoint string) *loggedServer { 365 | s := newServer(server.WithServiceAt(endpoint)) 366 | 367 | if err := s.s.ReadPolicies(strings.NewReader(p)); err != nil { 368 | t.Fatalf("can't read policies: %s", err) 369 | } 370 | 371 | if err := waitForPortClosed(endpoint); err != nil { 372 | t.Fatalf("port still in use: %s", err) 373 | } 374 | 375 | go func() { 376 | if err := s.s.Serve(); err != nil { 377 | t.Fatalf("PDP server failed: %s", err) 378 | } 379 | }() 380 | 381 | return s 382 | } 383 | 384 | type loggedServer struct { 385 | s *server.Server 386 | b *bytes.Buffer 387 | } 388 | 389 | func newServer(opts ...server.Option) *loggedServer { 390 | s := &loggedServer{ 391 | b: new(bytes.Buffer), 392 | } 393 | 394 | //logger := log.New() 395 | //logger.Out = s.b 396 | //logger.Level = log.ErrorLevel 397 | //opts = append(opts, 398 | // server.WithLogger(logger), 399 | //) 400 | 401 | s.s = server.NewServer(opts...) 402 | return s 403 | } 404 | 405 | func (s *loggedServer) Stop() string { 406 | s.s.Stop() 407 | return s.b.String() 408 | } 409 | 410 | func waitForPortOpened(address string) error { 411 | var ( 412 | c net.Conn 413 | err error 414 | ) 415 | 416 | for i := 0; i < 20; i++ { 417 | after := time.After(500 * time.Millisecond) 418 | c, err = net.DialTimeout("tcp", address, 500*time.Millisecond) 419 | if err == nil { 420 | return c.Close() 421 | } 422 | 423 | <-after 424 | } 425 | 426 | return err 427 | } 428 | 429 | func waitForPortClosed(address string) error { 430 | var ( 431 | c net.Conn 432 | err error 433 | ) 434 | 435 | for i := 0; i < 20; i++ { 436 | after := time.After(500 * time.Millisecond) 437 | c, err = net.DialTimeout("tcp", address, 500*time.Millisecond) 438 | if err != nil { 439 | return nil 440 | } 441 | 442 | c.Close() 443 | <-after 444 | } 445 | 446 | return fmt.Errorf("port at %s hasn't been closed yet", address) 447 | } 448 | -------------------------------------------------------------------------------- /plugin/themis/config.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/coredns/caddy" 11 | ) 12 | 13 | var errInvalidOption = errors.New("invalid themis plugin option") 14 | 15 | var allowedAttrTypes = map[string]string{ 16 | "string": "string", 17 | "domain": "domain", 18 | "address": "address"} 19 | 20 | type config struct { 21 | policyFile string 22 | contentFiles []string 23 | endpoints []string 24 | options []*attrSetting 25 | custAttrs map[string]custAttr 26 | debugID string 27 | debugSuffix string 28 | streams int 29 | hotSpot bool 30 | connTimeout time.Duration 31 | autoReqSize bool 32 | maxReqSize int 33 | autoResAttrs bool 34 | maxResAttrs int 35 | log bool 36 | cacheTTL time.Duration 37 | cacheLimit int 38 | } 39 | 40 | func themisParse(c *caddy.Controller) (*ThemisPlugin, error) { 41 | tp := newThemisPlugin() 42 | for c.Next() { 43 | args := c.RemainingArgs() 44 | if len(args) != 1 { 45 | return nil, c.Errf("themis plugin should have the format : themis {...} ") 46 | } 47 | name := args[0] 48 | if _, ok := tp.engines[name]; ok { 49 | return nil, c.Errf("themis plugin with engine name %s is already declared", name) 50 | } 51 | p := newThemisEngine() 52 | for c.NextBlock() { 53 | if err := p.conf.parseOption(c); err != nil { 54 | return nil, err 55 | } 56 | } 57 | tp.engines[name] = p 58 | } 59 | return tp, nil 60 | } 61 | 62 | func (conf *config) parseOption(c *caddy.Controller) error { 63 | switch c.Val() { 64 | case "pdp": 65 | return conf.parsePDP(c) 66 | 67 | case "endpoint": 68 | return conf.parseEndpoint(c) 69 | 70 | case "attr": 71 | return conf.parseAttr(c) 72 | 73 | case "debug_query_suffix": 74 | return conf.parseDebugQuerySuffix(c) 75 | 76 | case "streams": 77 | return conf.parseStreams(c) 78 | 79 | case "transfer": 80 | return conf.parseAttributes(c, custAttrTransfer) 81 | 82 | case "metrics": 83 | return conf.parseAttributes(c, custAttrMetrics) 84 | 85 | case "debug_id": 86 | return conf.parseDebugID(c) 87 | 88 | case "connection_timeout": 89 | return conf.parseConnectionTimeout(c) 90 | 91 | case "log": 92 | return conf.parseLog(c) 93 | 94 | case "max_request_size": 95 | return conf.parseMaxRequestSize(c) 96 | 97 | case "max_response_attributes": 98 | return conf.parseMaxResponseAttributes(c) 99 | 100 | case "cache": 101 | return conf.parseCache(c) 102 | } 103 | 104 | return errInvalidOption // TODO: anonymous error is lazy. Add invalid option name to error. 105 | } 106 | 107 | // Usage: pdp policy.[yaml|json] content1 content2... 108 | func (conf *config) parsePDP(c *caddy.Controller) error { 109 | args := c.RemainingArgs() 110 | argsLen := len(args) 111 | if argsLen < 1 { 112 | return c.ArgErr() 113 | } 114 | 115 | conf.policyFile = args[0] 116 | if argsLen > 1 { 117 | conf.contentFiles = args[1:] 118 | } 119 | return nil 120 | } 121 | 122 | func (conf *config) parseEndpoint(c *caddy.Controller) error { 123 | args := c.RemainingArgs() 124 | if len(args) <= 0 { 125 | return c.ArgErr() 126 | } 127 | 128 | conf.endpoints = args 129 | return nil 130 | } 131 | 132 | func (conf *config) parseAttr(c *caddy.Controller) error { 133 | args := c.RemainingArgs() 134 | // Valid destTypes depend on PDP (default string). 135 | argsLen := len(args) 136 | if argsLen != 2 && argsLen != 3 { 137 | return c.Errf("Invalid attr directive. Expected 2 or 3 arguments but got %d", argsLen) 138 | } 139 | 140 | name := args[0] 141 | label := args[1] 142 | dataType := "string" 143 | if argsLen > 2 { 144 | dataType = args[2] 145 | } 146 | 147 | if _, ok := allowedAttrTypes[strings.ToLower(dataType)]; !ok { 148 | tp := make([]string, 0) 149 | for k := range allowedAttrTypes { 150 | tp = append(tp, k) 151 | } 152 | return c.Errf("invalid type %s for an attribute - allowed types are : %s", dataType, strings.Join(tp, ",")) 153 | } 154 | conf.options = append(conf.options, &attrSetting{name, label, dataType, false}) 155 | conf.custAttrs[name] = conf.custAttrs[name] | custAttrEdns 156 | return nil 157 | } 158 | 159 | func (conf *config) parseAttributes(c *caddy.Controller, a custAttr) error { 160 | args := c.RemainingArgs() 161 | if len(args) <= 0 { 162 | return c.ArgErr() 163 | } 164 | 165 | for _, item := range args { 166 | conf.custAttrs[item] = conf.custAttrs[item] | a 167 | } 168 | 169 | return nil 170 | } 171 | 172 | func (conf *config) parseStreams(c *caddy.Controller) error { 173 | args := c.RemainingArgs() 174 | if len(args) < 1 || len(args) > 2 { 175 | return c.ArgErr() 176 | } 177 | 178 | streams, err := strconv.ParseInt(args[0], 10, 32) 179 | if err != nil { 180 | return c.Errf("Could not parse number of streams: %s", err) 181 | } 182 | if streams < 1 { 183 | return c.Errf("Expected at least one stream got %d", streams) 184 | } 185 | 186 | conf.streams = int(streams) 187 | 188 | if len(args) > 1 { 189 | switch strings.ToLower(args[1]) { 190 | default: 191 | return c.Errf("Expected round-robin or hot-spot balancing but got %s", args[1]) 192 | 193 | case "round-robin": 194 | conf.hotSpot = false 195 | 196 | case "hot-spot": 197 | conf.hotSpot = true 198 | } 199 | } else { 200 | conf.hotSpot = false 201 | } 202 | 203 | return nil 204 | } 205 | 206 | func (conf *config) parseDebugQuerySuffix(c *caddy.Controller) error { 207 | args := c.RemainingArgs() 208 | if len(args) != 1 { 209 | return c.ArgErr() 210 | } 211 | 212 | conf.debugSuffix = args[0] 213 | return nil 214 | } 215 | 216 | func (conf *config) parseDebugID(c *caddy.Controller) error { 217 | args := c.RemainingArgs() 218 | if len(args) != 1 { 219 | return c.ArgErr() 220 | } 221 | 222 | conf.debugID = args[0] 223 | return nil 224 | } 225 | 226 | func (conf *config) parseConnectionTimeout(c *caddy.Controller) error { 227 | args := c.RemainingArgs() 228 | if len(args) != 1 { 229 | return c.ArgErr() 230 | } 231 | 232 | if strings.ToLower(args[0]) == "no" { 233 | conf.connTimeout = -1 234 | } else { 235 | timeout, err := time.ParseDuration(args[0]) 236 | if err != nil { 237 | return c.Errf("Could not parse timeout: %s", err) 238 | } 239 | 240 | conf.connTimeout = timeout 241 | } 242 | 243 | return nil 244 | } 245 | 246 | func (conf *config) parseLog(c *caddy.Controller) error { 247 | args := c.RemainingArgs() 248 | if len(args) != 0 { 249 | return c.ArgErr() 250 | } 251 | 252 | conf.log = true 253 | return nil 254 | } 255 | 256 | func (conf *config) parseMaxRequestSize(c *caddy.Controller) error { 257 | args := c.RemainingArgs() 258 | if len(args) < 1 || len(args) > 2 { 259 | return c.ArgErr() 260 | } 261 | 262 | s := "" 263 | if strings.ToLower(args[0]) == "auto" { 264 | conf.autoReqSize = true 265 | if len(args) > 1 { 266 | s = args[1] 267 | } 268 | } else { 269 | s = args[0] 270 | } 271 | 272 | if len(s) > 0 { 273 | size, err := strconv.ParseUint(s, 10, 0) 274 | if err != nil { 275 | return c.Errf("Could not parse PDP request size limit: %s", err) 276 | } 277 | 278 | if size > math.MaxInt32 { 279 | return c.Errf("Size limit %d (> %d) for PDP request is too high", size, math.MaxInt32) 280 | } 281 | 282 | conf.maxReqSize = int(size) 283 | } 284 | 285 | return nil 286 | } 287 | 288 | func (conf *config) parseMaxResponseAttributes(c *caddy.Controller) error { 289 | args := c.RemainingArgs() 290 | if len(args) != 1 { 291 | return c.ArgErr() 292 | } 293 | 294 | if strings.ToLower(args[0]) == "auto" { 295 | conf.autoResAttrs = true 296 | return nil 297 | } 298 | 299 | n, err := strconv.ParseUint(args[0], 10, 0) 300 | if err != nil { 301 | return c.Errf("Could not parse PDP response attributes limit: %s", err) 302 | } 303 | 304 | if n > math.MaxInt32 { 305 | return c.Errf("Attributes limit %d (> %d) for PDP response is too high", n, math.MaxInt32) 306 | } 307 | 308 | conf.maxResAttrs = int(n) 309 | return nil 310 | } 311 | 312 | func (conf *config) parseCache(c *caddy.Controller) error { 313 | args := c.RemainingArgs() 314 | if len(args) > 2 { 315 | return c.ArgErr() 316 | } 317 | 318 | if len(args) > 0 { 319 | ttl, err := time.ParseDuration(args[0]) 320 | if err != nil { 321 | return c.Errf("Could not parse decision cache TTL: %s", err) 322 | } 323 | 324 | if ttl <= 0 { 325 | return c.Errf("Can't set decision cache TTL to %s", ttl) 326 | } 327 | 328 | conf.cacheTTL = ttl 329 | } else { 330 | conf.cacheTTL = 10 * time.Minute 331 | } 332 | 333 | if len(args) > 1 { 334 | n, err := strconv.ParseUint(args[1], 10, 0) 335 | if err != nil { 336 | return c.Errf("Could not parse decision cache limit: %s", err) 337 | } 338 | 339 | if n > math.MaxInt32 { 340 | return c.Errf("Cache limit %d (> %d) is too high", n, math.MaxInt32) 341 | } 342 | 343 | conf.cacheLimit = int(n) 344 | } 345 | 346 | return nil 347 | } 348 | -------------------------------------------------------------------------------- /plugin/themis/config_test.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/coredns/caddy" 10 | "github.com/coredns/caddy/caddyfile" 11 | ) 12 | 13 | func TestThemisConfigParse(t *testing.T) { 14 | tests := []struct { 15 | desc string 16 | input string 17 | err error 18 | 19 | endpoints []string 20 | options []*attrSetting 21 | debugSuffix *string 22 | streams *int 23 | hotSpot *bool 24 | custAttrs map[string]custAttr 25 | debugID *string 26 | passthrough []string 27 | connTimeout *time.Duration 28 | autoReqSize *bool 29 | maxReqSize *int 30 | autoResAttrs *bool 31 | maxResAttrs *int 32 | cacheTTL *time.Duration 33 | cacheLimit *int 34 | }{ 35 | { 36 | desc: "InvalidOption", 37 | input: `.:53 { 38 | themis NAME { 39 | error option 40 | } 41 | }`, 42 | err: errors.New("invalid themis plugin option"), 43 | }, 44 | { 45 | desc: "NoEndpointArguemnts", 46 | input: `.:53 { 47 | themis NAME { 48 | endpoint 49 | } 50 | }`, 51 | err: errors.New("Wrong argument count or unexpected line ending"), 52 | }, 53 | { 54 | desc: "SingleEntryEndpoint", 55 | input: `.:53 { 56 | themis NAME { 57 | endpoint 10.2.4.1:5555 58 | } 59 | }`, 60 | endpoints: []string{"10.2.4.1:5555"}, 61 | }, 62 | { 63 | desc: "ThreeEntriesEndpoint", 64 | input: `.:53 { 65 | themis NAME { 66 | endpoint 10.2.4.1:5555 10.2.4.2:5555 67 | } 68 | }`, 69 | endpoints: []string{"10.2.4.1:5555", "10.2.4.2:5555"}, 70 | }, 71 | { 72 | desc: "InvalidAttrType", 73 | input: `.:53 { 74 | themis NAME { 75 | endpoint 10.2.4.1:5555 76 | attr uid request/uid no-type 77 | } 78 | }`, 79 | err: errors.New("invalid type"), 80 | }, 81 | { 82 | desc: "AttrCorrect", 83 | input: `.:53 { 84 | themis NAME { 85 | endpoint 10.2.4.1:5555 86 | attr uid request/uid 87 | } 88 | }`, 89 | options: []*attrSetting{ 90 | {"uid", "request/uid", "string", false}, 91 | }, 92 | custAttrs: map[string]custAttr{ 93 | "uid": custAttrEdns, 94 | }, 95 | }, 96 | { 97 | desc: "AttrCorrect2values", 98 | input: `.:53 { 99 | themis NAME { 100 | endpoint 10.2.4.1:5555 101 | attr uid request/uid 102 | attr ip request/ip address 103 | } 104 | }`, 105 | options: []*attrSetting{ 106 | {"uid", "request/uid", "string", false}, 107 | {"ip", "request/ip", "address", false}, 108 | }, 109 | custAttrs: map[string]custAttr{ 110 | "uid": custAttrEdns, 111 | "ip": custAttrEdns, 112 | }, 113 | }, 114 | { 115 | desc: "AttrWithNoLabel", 116 | input: `.:53 { 117 | themis NAME { 118 | endpoint 10.2.4.1:5555 119 | attr my-name 120 | } 121 | }`, 122 | err: errors.New("Invalid attr directive"), 123 | }, 124 | { 125 | desc: "NoDebugQuerySuffixArguments", 126 | input: `.:53 { 127 | themis NAME { 128 | endpoint 10.2.4.1:5555 129 | debug_query_suffix 130 | } 131 | }`, 132 | err: errors.New("Wrong argument count or unexpected line ending"), 133 | }, 134 | { 135 | desc: "DebugQuerySuffix", 136 | input: `.:53 { 137 | themis NAME { 138 | endpoint 10.2.4.1:5555 139 | debug_query_suffix debug.local. 140 | } 141 | }`, 142 | debugSuffix: newStringPtr("debug.local."), 143 | }, 144 | { 145 | desc: "PDPClientStreams", 146 | input: `.:53 { 147 | themis NAME { 148 | endpoint 10.2.4.1:5555 149 | streams 10 150 | } 151 | }`, 152 | streams: newIntPtr(10), 153 | }, 154 | { 155 | desc: "InvalidPDPClientStreams", 156 | input: `.:53 { 157 | themis NAME { 158 | endpoint 10.2.4.1:5555 159 | streams Ten 160 | } 161 | }`, 162 | err: errors.New("Could not parse number of streams"), 163 | }, 164 | { 165 | desc: "NoPDPClientStreamsArguments", 166 | input: `.:53 { 167 | themis NAME { 168 | endpoint 10.2.4.1:5555 169 | streams 170 | } 171 | }`, 172 | err: errors.New("Wrong argument count or unexpected line ending"), 173 | }, 174 | { 175 | desc: "NegativePDPClientStreams", 176 | input: `.:53 { 177 | themis NAME { 178 | endpoint 10.2.4.1:5555 179 | streams -1 180 | } 181 | }`, 182 | err: errors.New("Expected at least one stream got -1"), 183 | }, 184 | { 185 | desc: "PDPClientStreamsWithRoundRobin", 186 | input: `.:53 { 187 | themis NAME { 188 | endpoint 10.2.4.1:5555 189 | streams 10 Round-Robin 190 | } 191 | }`, 192 | streams: newIntPtr(10), 193 | hotSpot: newBoolPtr(false), 194 | }, 195 | { 196 | desc: "PDPClientStreamsWithHotSpot", 197 | input: `.:53 { 198 | themis NAME { 199 | endpoint 10.2.4.1:5555 200 | streams 10 Hot-Spot 201 | } 202 | }`, 203 | streams: newIntPtr(10), 204 | hotSpot: newBoolPtr(true), 205 | }, 206 | { 207 | desc: "InvalidPDPClientStreamsBalancer", 208 | input: `.:53 { 209 | themis NAME { 210 | endpoint 10.2.4.1:5555 211 | streams 10 Unknown-Balancer 212 | } 213 | }`, 214 | err: errors.New("Expected round-robin or hot-spot balancing but got Unknown-Balancer"), 215 | }, 216 | { 217 | desc: "TransferAttribute", 218 | input: `.:53 { 219 | themis NAME { 220 | endpoint 10.2.4.1:5555 221 | transfer themis_id 222 | } 223 | }`, 224 | custAttrs: map[string]custAttr{ 225 | "themis_id": custAttrTransfer, 226 | }, 227 | }, 228 | { 229 | desc: "ComplexAttributeConfig", 230 | input: `.:53 { 231 | themis NAME { 232 | endpoint 10.2.4.1:5555 233 | attr uid request/uid 234 | attr id request/id 235 | transfer themis_id id 236 | } 237 | }`, 238 | options: []*attrSetting{ 239 | {"uid", "request/uid", "string", false}, 240 | {"id", "request/id", "string", false}, 241 | }, 242 | custAttrs: map[string]custAttr{ 243 | "themis_id": custAttrTransfer, 244 | "id": custAttrEdns | custAttrTransfer, 245 | "uid": custAttrEdns, 246 | }, 247 | }, 248 | { 249 | desc: "NoTransferArguments", 250 | input: `.:53 { 251 | themis NAME { 252 | endpoint 10.2.4.1:5555 253 | transfer 254 | } 255 | }`, 256 | err: errors.New("Wrong argument count or unexpected line ending"), 257 | }, 258 | { 259 | desc: "DebugID", 260 | input: `.:53 { 261 | themis NAME { 262 | endpoint 10.2.4.1:5555 263 | metrics 264 | } 265 | }`, 266 | err: errors.New("Wrong argument count or unexpected line ending"), 267 | }, 268 | { 269 | desc: "ComplexAttributeConfigWithMetrics", 270 | input: `.:53 { 271 | themis NAME { 272 | endpoint 10.2.4.1:5555 273 | attr uid request/uid 274 | attr id request/id 275 | metrics uid query_id 276 | } 277 | }`, 278 | options: []*attrSetting{ 279 | {"uid", "request/uid", "string", false}, 280 | {"id", "request/id", "string", false}, 281 | }, 282 | custAttrs: map[string]custAttr{ 283 | "id": custAttrEdns, 284 | "uid": custAttrEdns | custAttrMetrics, 285 | "query_id": custAttrMetrics, 286 | }, 287 | }, 288 | { 289 | input: `.:53 { 290 | themis NAME { 291 | endpoint 10.2.4.1:5555 292 | debug_id corednsinstance 293 | } 294 | }`, 295 | debugID: newStringPtr("corednsinstance"), 296 | }, 297 | { 298 | desc: "NoDebugIDArguments", 299 | input: `.:53 { 300 | themis NAME { 301 | endpoint 10.2.4.1:5555 302 | debug_id 303 | } 304 | }`, 305 | err: errors.New("Wrong argument count or unexpected line ending"), 306 | }, 307 | { 308 | desc: "NoConnectionTimeoutArguments", 309 | input: `.:53 { 310 | themis NAME { 311 | connection_timeout 312 | } 313 | }`, 314 | err: errors.New("Wrong argument count or unexpected line ending"), 315 | }, 316 | { 317 | desc: "NoConnectionTimeout", 318 | input: `.:53 { 319 | themis NAME { 320 | endpoint 10.2.4.1:5555 321 | connection_timeout no 322 | } 323 | }`, 324 | connTimeout: newDurationPtr(-1), 325 | }, 326 | { 327 | desc: "ConnectionTimeout", 328 | input: `.:53 { 329 | themis NAME { 330 | endpoint 10.2.4.1:5555 331 | connection_timeout 500ms 332 | } 333 | }`, 334 | connTimeout: newDurationPtr(500 * time.Millisecond), 335 | }, 336 | { 337 | desc: "InvalidConnectionTimeout", 338 | input: `.:53 { 339 | themis NAME { 340 | endpoint 10.2.4.1:5555 341 | connection_timeout invalid 342 | } 343 | }`, 344 | err: errors.New("Could not parse timeout: time: invalid duration \"invalid\""), 345 | }, 346 | { 347 | desc: "Log", 348 | input: `.:53 { 349 | themis NAME { 350 | endpoint 10.2.4.1:5555 351 | log 352 | } 353 | }`, 354 | }, 355 | { 356 | desc: "TrailingLogArgument", 357 | input: `.:53 { 358 | themis NAME { 359 | endpoint 10.2.4.1:5555 360 | log stdout 361 | } 362 | }`, 363 | err: errors.New("Wrong argument count or unexpected line ending"), 364 | }, 365 | { 366 | desc: "MaxRequestSize", 367 | input: `.:53 { 368 | themis NAME { 369 | endpoint 10.2.4.1:5555 370 | max_request_size 128 371 | } 372 | }`, 373 | autoReqSize: newBoolPtr(false), 374 | maxReqSize: newIntPtr(128), 375 | }, 376 | { 377 | desc: "MaxRequestSize", 378 | input: `.:53 { 379 | themis NAME { 380 | endpoint 10.2.4.1:5555 381 | max_request_size auto 382 | } 383 | }`, 384 | autoReqSize: newBoolPtr(true), 385 | maxReqSize: newIntPtr(-1), 386 | }, 387 | { 388 | desc: "MaxRequestSize", 389 | input: `.:53 { 390 | themis NAME { 391 | endpoint 10.2.4.1:5555 392 | max_request_size auto 128 393 | } 394 | }`, 395 | autoReqSize: newBoolPtr(true), 396 | maxReqSize: newIntPtr(128), 397 | }, 398 | { 399 | desc: "NoMaxRequestSizeArguments", 400 | input: `.:53 { 401 | themis NAME { 402 | endpoint 10.2.4.1:5555 403 | max_request_size 404 | } 405 | }`, 406 | err: errors.New("Wrong argument count or unexpected line ending"), 407 | }, 408 | { 409 | desc: "InvalidMaxRequestSize", 410 | input: `.:53 { 411 | themis NAME { 412 | endpoint 10.2.4.1:5555 413 | max_request_size test 414 | } 415 | }`, 416 | err: errors.New("Could not parse PDP request size limit"), 417 | }, 418 | { 419 | desc: "OverflowMaxRequestSize", 420 | input: `.:53 { 421 | themis NAME { 422 | endpoint 10.2.4.1:5555 423 | max_request_size 2147483648 424 | } 425 | }`, 426 | err: errors.New("Size limit 2147483648 (> 2147483647) for PDP request is too high"), 427 | }, 428 | { 429 | desc: "MaxResponseAttributes", 430 | input: `.:53 { 431 | themis NAME { 432 | endpoint 10.2.4.1:5555 433 | max_response_attributes 128 434 | } 435 | }`, 436 | autoResAttrs: newBoolPtr(false), 437 | maxResAttrs: newIntPtr(128), 438 | }, 439 | { 440 | desc: "MaxResponseAttributes", 441 | input: `.:53 { 442 | themis NAME { 443 | endpoint 10.2.4.1:5555 444 | max_response_attributes auto 445 | } 446 | }`, 447 | autoResAttrs: newBoolPtr(true), 448 | maxResAttrs: newIntPtr(64), 449 | }, 450 | { 451 | desc: "NoMaxResponseAttributesArguments", 452 | input: `.:53 { 453 | themis NAME { 454 | endpoint 10.2.4.1:5555 455 | max_response_attributes 456 | } 457 | }`, 458 | err: errors.New("Wrong argument count or unexpected line ending"), 459 | }, 460 | { 461 | desc: "InvalidMaxResponseAttributes", 462 | input: `.:53 { 463 | themis NAME { 464 | endpoint 10.2.4.1:5555 465 | max_response_attributes invalid 466 | } 467 | }`, 468 | err: errors.New("Could not parse PDP response attributes limit"), 469 | }, 470 | { 471 | desc: "OverflowMaxResponseAttributes", 472 | input: `.:53 { 473 | themis NAME { 474 | endpoint 10.2.4.1:5555 475 | max_response_attributes 2147483648 476 | } 477 | }`, 478 | err: errors.New("Attributes limit 2147483648 (> 2147483647) for PDP response is too high"), 479 | }, 480 | { 481 | desc: "NoDecisionCache", 482 | input: `.:53 { 483 | themis NAME { 484 | endpoint 10.2.4.1:5555 485 | } 486 | }`, 487 | cacheTTL: newDurationPtr(0), 488 | }, 489 | { 490 | desc: "DecisionCache", 491 | input: `.:53 { 492 | themis NAME { 493 | endpoint 10.2.4.1:5555 494 | cache 495 | } 496 | }`, 497 | cacheTTL: newDurationPtr(10 * time.Minute), 498 | cacheLimit: newIntPtr(0), 499 | }, 500 | { 501 | desc: "DecisionCacheWithTTL", 502 | input: `.:53 { 503 | themis NAME { 504 | endpoint 10.2.4.1:5555 505 | cache 15s 506 | } 507 | }`, 508 | cacheTTL: newDurationPtr(15 * time.Second), 509 | cacheLimit: newIntPtr(0), 510 | }, 511 | { 512 | desc: "DecisionCacheWithTTLAndLimit", 513 | input: `.:53 { 514 | themis NAME { 515 | endpoint 10.2.4.1:5555 516 | cache 15s 128 517 | } 518 | }`, 519 | cacheTTL: newDurationPtr(15 * time.Second), 520 | cacheLimit: newIntPtr(128), 521 | }, 522 | { 523 | desc: "TooManyCacheArguments", 524 | input: `.:53 { 525 | themis NAME { 526 | endpoint 10.2.4.1:5555 527 | cache too many of them 528 | } 529 | }`, 530 | err: errors.New("Wrong argument count or unexpected line ending"), 531 | }, 532 | { 533 | desc: "InvalidCacheTTL", 534 | input: `.:53 { 535 | themis NAME { 536 | endpoint 10.2.4.1:5555 537 | cache invalid 538 | } 539 | }`, 540 | err: errors.New("Could not parse decision cache TTL"), 541 | }, 542 | { 543 | desc: "WrongCacheTTL", 544 | input: `.:53 { 545 | themis NAME { 546 | endpoint 10.2.4.1:5555 547 | cache -15s 548 | } 549 | }`, 550 | err: errors.New("Can't set decision cache TTL to"), 551 | }, 552 | { 553 | desc: "InvalidCacheLimit", 554 | input: `.:53 { 555 | themis NAME { 556 | endpoint 10.2.4.1:5555 557 | cache 15s invalid 558 | } 559 | }`, 560 | err: errors.New("Could not parse decision cache limit"), 561 | }, 562 | { 563 | desc: "OverflowCacheLimit", 564 | input: `.:53 { 565 | themis NAME { 566 | endpoint 10.2.4.1:5555 567 | cache 15s 2147483648 568 | } 569 | }`, 570 | err: errors.New("Cache limit 2147483648 (> 2147483647) is too high"), 571 | }, 572 | } 573 | 574 | for _, test := range tests { 575 | t.Run(test.desc, func(t *testing.T) { 576 | blocs, err := caddyfile.Parse("test-file", strings.NewReader(test.input), []string{"themis"}) 577 | d := caddyfile.NewDispenserTokens("themis", blocs[0].Tokens["themis"]) 578 | c := &caddy.Controller{Dispenser: d} 579 | mw, err := themisParse(c) 580 | if err != nil { 581 | if test.err != nil { 582 | if !strings.Contains(err.Error(), test.err.Error()) { 583 | t.Errorf("Expected error '%v' but got '%v'\n", test.err, err) 584 | } 585 | } else { 586 | t.Errorf("Expected no error but got '%v'\n", err) 587 | } 588 | } else { 589 | // we consider only one Engine declared 590 | for _, mwe := range mw.engines { 591 | if test.err != nil { 592 | t.Errorf("Expected error '%v' but got 'nil'\n", test.err) 593 | } else { 594 | if test.endpoints != nil { 595 | if len(test.endpoints) != len(mwe.conf.endpoints) { 596 | t.Errorf("Expected endpoints %v but got %v\n", test.endpoints, mwe.conf.endpoints) 597 | } else { 598 | for i := 0; i < len(test.endpoints); i++ { 599 | if test.endpoints[i] != mwe.conf.endpoints[i] { 600 | t.Errorf("Expected endpoint '%s' but got '%s'\n", 601 | test.endpoints[i], mwe.conf.endpoints[i]) 602 | } 603 | } 604 | } 605 | } 606 | 607 | if test.options != nil { 608 | if len(test.options) != len(mwe.conf.options) { 609 | t.Errorf("Expected %d Attr options but got %d", 610 | len(test.options), len(mwe.conf.options)) 611 | } else { 612 | for k, testAttr := range test.options { 613 | mwOpt := mwe.conf.options[k] 614 | if testAttr.name != mwOpt.name || 615 | testAttr.label != mwOpt.label || 616 | testAttr.attrType != mwOpt.attrType || 617 | testAttr.metrics != mwOpt.metrics { 618 | t.Errorf("Expected Attr option:\n\t\"%#v\""+ 619 | "\nfor but got:\n\t\"%#v\"", 620 | *testAttr, *mwOpt) 621 | } 622 | } 623 | 624 | } 625 | } 626 | 627 | if test.debugSuffix != nil && *test.debugSuffix != mwe.conf.debugSuffix { 628 | t.Errorf("Expected debug suffix %q but got %q", *test.debugSuffix, mwe.conf.debugSuffix) 629 | } 630 | 631 | if test.streams != nil && *test.streams != mwe.conf.streams { 632 | t.Errorf("Expected %d streams but got %d", *test.streams, mwe.conf.streams) 633 | } 634 | 635 | if test.hotSpot != nil && *test.hotSpot != mwe.conf.hotSpot { 636 | t.Errorf("Expected hotSpot=%v but got %v", *test.hotSpot, mwe.conf.hotSpot) 637 | } 638 | 639 | if test.custAttrs != nil { 640 | for k, et := range test.custAttrs { 641 | at, ok := mwe.conf.custAttrs[k] 642 | if !ok { 643 | t.Errorf("Missing conf attribute %q", k) 644 | } else if et != at { 645 | t.Errorf("Unexpected type of conf attribute %q; expected=%d, actual=%d", k, et, at) 646 | } 647 | } 648 | 649 | for k, at := range mwe.conf.custAttrs { 650 | if _, ok := test.custAttrs[k]; !ok { 651 | t.Errorf("Unexpected conf attribute %q=%d", k, at) 652 | } 653 | } 654 | } 655 | 656 | if test.debugID != nil && *test.debugID != mwe.conf.debugID { 657 | t.Errorf("Expected debug id %q but got %q", *test.debugID, mwe.conf.debugID) 658 | } 659 | 660 | if test.connTimeout != nil && *test.connTimeout != mwe.conf.connTimeout { 661 | t.Errorf("Expected connection timeout %s but got %s", *test.connTimeout, mwe.conf.connTimeout) 662 | } 663 | 664 | if test.autoReqSize != nil && *test.autoReqSize != mwe.conf.autoReqSize { 665 | t.Errorf("Expected automatic request size %v but got %v", 666 | *test.autoReqSize, mwe.conf.autoReqSize) 667 | } 668 | 669 | if test.maxReqSize != nil && *test.maxReqSize != mwe.conf.maxReqSize { 670 | t.Errorf("Expected request size limit %d but got %d", *test.maxReqSize, mwe.conf.maxReqSize) 671 | } 672 | 673 | if test.autoResAttrs != nil && *test.autoResAttrs != mwe.conf.autoResAttrs { 674 | t.Errorf("Expected automatic response attributes %v but got %v", 675 | *test.autoResAttrs, mwe.conf.autoResAttrs) 676 | } 677 | 678 | if test.maxResAttrs != nil && *test.maxResAttrs != mwe.conf.maxResAttrs { 679 | t.Errorf("Expected response attributes limit %d but got %d", 680 | *test.maxResAttrs, mwe.conf.maxResAttrs) 681 | } 682 | 683 | if test.cacheTTL != nil && *test.cacheTTL != mwe.conf.cacheTTL { 684 | t.Errorf("Expected cache TTL %s but got %s", *test.cacheTTL, mwe.conf.cacheTTL) 685 | } 686 | 687 | if test.cacheLimit != nil && *test.cacheLimit != mwe.conf.cacheLimit { 688 | t.Errorf("Expected cache limit %d but got %d", *test.cacheLimit, mwe.conf.cacheLimit) 689 | } 690 | } 691 | } 692 | } 693 | }) 694 | } 695 | } 696 | 697 | func newStringPtr(s string) *string { 698 | return &s 699 | } 700 | 701 | func newIntPtr(n int) *int { 702 | return &n 703 | } 704 | 705 | func newBoolPtr(b bool) *bool { 706 | return &b 707 | } 708 | 709 | func newDurationPtr(d time.Duration) *time.Duration { 710 | return &d 711 | } 712 | -------------------------------------------------------------------------------- /plugin/themis/dns_test.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | func makeTestDNSMsg(n string, t uint16, c uint16) *dns.Msg { 10 | out := new(dns.Msg) 11 | out.Question = make([]dns.Question, 1) 12 | out.Question[0] = dns.Question{ 13 | Name: dns.Fqdn(n), 14 | Qtype: t, 15 | Qclass: c, 16 | } 17 | return out 18 | } 19 | 20 | func appendAnswer(m *dns.Msg, rr ...dns.RR) { 21 | if m.Answer == nil { 22 | m.Answer = []dns.RR{} 23 | } 24 | 25 | m.Answer = append(m.Answer, rr...) 26 | } 27 | 28 | func newA(a net.IP) dns.RR { 29 | out := new(dns.A) 30 | out.Hdr.Name = "." 31 | out.Hdr.Rrtype = dns.TypeA 32 | out.A = a 33 | 34 | return out 35 | } 36 | 37 | func newAAAA(a net.IP) dns.RR { 38 | out := new(dns.AAAA) 39 | out.Hdr.Name = "." 40 | out.Hdr.Rrtype = dns.TypeAAAA 41 | out.AAAA = a 42 | 43 | return out 44 | } 45 | 46 | func newCNAME(s string) dns.RR { 47 | out := new(dns.CNAME) 48 | out.Hdr.Name = "." 49 | out.Hdr.Rrtype = dns.TypeCNAME 50 | out.Target = dns.Fqdn(s) 51 | 52 | return out 53 | } 54 | 55 | func makeTestDNSMsgWithEdns0(n string, t uint16, c uint16, o ...*dns.OPT) *dns.Msg { 56 | out := makeTestDNSMsg(n, t, c) 57 | 58 | extra := make([]dns.RR, len(o)) 59 | for i, o := range o { 60 | extra[i] = o 61 | } 62 | 63 | out.Extra = extra 64 | return out 65 | } 66 | 67 | func newEdns0(o ...dns.EDNS0) *dns.OPT { 68 | out := new(dns.OPT) 69 | out.Hdr.Name = "." 70 | out.Hdr.Rrtype = dns.TypeOPT 71 | out.Option = o 72 | 73 | return out 74 | } 75 | 76 | func copyEdns0(in ...*dns.OPT) []*dns.OPT { 77 | out := make([]*dns.OPT, len(in)) 78 | for i, o := range in { 79 | out[i] = new(dns.OPT) 80 | out[i].Hdr = o.Hdr 81 | out[i].Option = make([]dns.EDNS0, len(o.Option)) 82 | copy(out[i].Option, o.Option) 83 | } 84 | 85 | return out 86 | } 87 | 88 | func newEdns0Cookie(s string) dns.EDNS0 { 89 | out := new(dns.EDNS0_COOKIE) 90 | out.Code = dns.EDNS0COOKIE 91 | out.Cookie = s 92 | 93 | return out 94 | } 95 | 96 | func newEdns0Local(c uint16, b []byte) dns.EDNS0 { 97 | out := new(dns.EDNS0_LOCAL) 98 | out.Code = c 99 | out.Data = b 100 | 101 | return out 102 | } 103 | 104 | func newEdns0Subnet(ip net.IP) dns.EDNS0 { 105 | out := new(dns.EDNS0_SUBNET) 106 | out.Code = dns.EDNS0SUBNET 107 | if ipv4 := ip.To4(); ipv4 != nil { 108 | out.Family = 1 109 | out.SourceNetmask = 32 110 | out.Address = ipv4 111 | } else if ipv6 := ip.To16(); ipv6 != nil { 112 | out.Family = 2 113 | out.SourceNetmask = 128 114 | out.Address = ipv6 115 | } 116 | out.SourceScope = 0 117 | 118 | return out 119 | } 120 | 121 | type testAddressedNonwriter struct { 122 | dns.ResponseWriter 123 | ra net.Addr 124 | Msg *dns.Msg 125 | } 126 | 127 | type testUDPAddr struct { 128 | addr string 129 | } 130 | 131 | func newTestAddressedNonwriter(ra string) *testAddressedNonwriter { 132 | return &testAddressedNonwriter{ 133 | ResponseWriter: nil, 134 | ra: newUDPAddr(ra), 135 | } 136 | } 137 | 138 | func newTestAddressedNonwriterWithAddr(ra net.Addr) *testAddressedNonwriter { 139 | return &testAddressedNonwriter{ 140 | ResponseWriter: nil, 141 | ra: ra, 142 | } 143 | } 144 | 145 | func (w *testAddressedNonwriter) RemoteAddr() net.Addr { 146 | return w.ra 147 | } 148 | 149 | func (w *testAddressedNonwriter) WriteMsg(res *dns.Msg) error { 150 | w.Msg = res 151 | return nil 152 | } 153 | 154 | func newUDPAddr(addr string) *testUDPAddr { 155 | return &testUDPAddr{ 156 | addr: addr, 157 | } 158 | } 159 | 160 | func (a *testUDPAddr) String() string { 161 | return a.addr 162 | } 163 | 164 | func (a *testUDPAddr) Network() string { 165 | return "udp" 166 | } 167 | -------------------------------------------------------------------------------- /plugin/themis/metrics.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/coredns/caddy" 11 | "github.com/coredns/coredns/core/dnsserver" 12 | "github.com/coredns/coredns/plugin" 13 | "github.com/coredns/coredns/plugin/metrics" 14 | "github.com/infobloxopen/themis/pdp" 15 | "github.com/prometheus/client_golang/prometheus" 16 | ) 17 | 18 | const ( 19 | BucketCnt = 16 20 | FreshCnt = 10 21 | ) 22 | 23 | // SlicedCounter stores the counter value both as a single number and as a separate 24 | // values per second. The sum of values per second is synchronized with total value 25 | // but not guaranteed to be equal at any moment of time 26 | type SlicedCounter struct { 27 | oldestValid uint32 28 | total uint32 29 | buckets [BucketCnt]uint32 30 | } 31 | 32 | // NewSlicedCounter creates new SlicedCounter 33 | func NewSlicedCounter(ut uint32) *SlicedCounter { 34 | return &SlicedCounter{oldestValid: ut} 35 | } 36 | 37 | // Total returns the counter value 38 | func (sc *SlicedCounter) Total() uint32 { 39 | return atomic.LoadUint32(&sc.total) 40 | } 41 | 42 | // Inc increments the latest and total counters. Can be called simultaneously 43 | // from different goroutines 44 | func (sc *SlicedCounter) Inc(ut uint32) bool { 45 | oldest := atomic.LoadUint32(&sc.oldestValid) 46 | if ut-oldest >= BucketCnt { 47 | return false 48 | } 49 | atomic.AddUint32(&sc.total, 1) 50 | atomic.AddUint32(&sc.buckets[ut%BucketCnt], 1) 51 | return true 52 | } 53 | 54 | // EraseStale erases the values from stale buckets, decrements the total counter 55 | // by the sum of erased values, and updates the oldestValid time. Should be run 56 | // in single goroutine 57 | func (sc *SlicedCounter) EraseStale(ut uint32) { 58 | oldest := atomic.LoadUint32(&sc.oldestValid) 59 | stale := ut - FreshCnt 60 | if stale >= oldest+BucketCnt { 61 | oldest = stale - BucketCnt + 1 62 | atomic.StoreUint32(&sc.oldestValid, oldest) 63 | } 64 | for oldest <= stale { 65 | cnt := atomic.SwapUint32(&sc.buckets[oldest%BucketCnt], 0) 66 | atomic.AddUint32(&sc.total, -cnt) 67 | atomic.AddUint32(&sc.oldestValid, 1) 68 | oldest++ 69 | } 70 | } 71 | 72 | const ( 73 | AttrGaugeStopped = iota 74 | AttrGaugeStarted 75 | AttrGaugeStopping 76 | ) 77 | 78 | const ( 79 | DefaultEraseInterval = 500 * time.Millisecond 80 | DefaultQueryChanSize = 1000 81 | ) 82 | 83 | // AttrGauge manages GaugeVec for attributes. GaugeVec holds the 84 | // counters for recently received (last FreshCnt seconds) DNS queries 85 | // per attribute/value 86 | type AttrGauge struct { 87 | perAttr map[string]map[string]*SlicedCounter 88 | pgv *prometheus.GaugeVec 89 | qChan chan pdp.AttributeAssignment 90 | nameChan chan string 91 | timeFunc func() uint32 92 | errCnt uint32 93 | state uint32 94 | } 95 | 96 | // NewAttrGauge constructs new AttrGauge object 97 | func NewAttrGauge() *AttrGauge { 98 | return &AttrGauge{ 99 | perAttr: make(map[string]map[string]*SlicedCounter), 100 | pgv: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 101 | Namespace: plugin.Namespace, 102 | Subsystem: "policy", 103 | Name: "recent_queries", 104 | Help: "Gauge of recent queries per Attrubute value.", 105 | }, []string{"attribute", "value"}), 106 | qChan: make(chan pdp.AttributeAssignment), 107 | nameChan: make(chan string), 108 | timeFunc: unixTime, 109 | } 110 | } 111 | 112 | // globalAttrGauge object is used across all policy plugin instances 113 | // including instances from different corefile blocks and 114 | // new/old instances during graceful restart 115 | var globalAttrGauge = NewAttrGauge() 116 | 117 | // Start starts goroutine which reads and handles data from channels 118 | func (g *AttrGauge) Start(tickInt time.Duration, chSize int) { 119 | if atomic.CompareAndSwapUint32(&g.state, AttrGaugeStopped, AttrGaugeStarted) { 120 | ch := make(chan pdp.AttributeAssignment, chSize) 121 | g.qChan = ch 122 | go func() { 123 | timer := time.NewTimer(tickInt) 124 | for { 125 | if atomic.CompareAndSwapUint32(&g.state, AttrGaugeStopping, AttrGaugeStopped) { 126 | break 127 | } 128 | select { 129 | case name := <-g.nameChan: 130 | g.addAttribute(name) 131 | case attr := <-ch: 132 | g.synchInc(attr) 133 | case <-timer.C: 134 | eCnt := g.tick() 135 | if eCnt != 0 { 136 | log.Printf("[WARN] Policy metrics: %d queries was skipped", eCnt) 137 | } 138 | timer.Reset(tickInt) 139 | } 140 | } 141 | }() 142 | } 143 | } 144 | 145 | // Stop stops goroutine which reads and handles data from channels 146 | func (g *AttrGauge) Stop() { 147 | if g == nil { 148 | return 149 | } 150 | if !atomic.CompareAndSwapUint32(&g.state, AttrGaugeStarted, AttrGaugeStopping) { 151 | return 152 | } 153 | for atomic.LoadUint32(&g.state) != AttrGaugeStopped { 154 | time.Sleep(10 * time.Millisecond) 155 | } 156 | } 157 | 158 | // AddAttribute adds new attribute names to gauge. It's safe to call it from 159 | // any goroutine. The AttrGauge should be started before calling AddAttributes 160 | func (g *AttrGauge) AddAttributes(attrNames ...string) { 161 | for _, name := range attrNames { 162 | g.nameChan <- name 163 | } 164 | } 165 | 166 | // addAttribute adds new attribute name to gauge. Should be called synchronously 167 | func (g *AttrGauge) addAttribute(attrName string) { 168 | if g.perAttr[attrName] == nil { 169 | g.perAttr[attrName] = make(map[string]*SlicedCounter) 170 | } 171 | } 172 | 173 | // Inc increments the counter corresponding to the attr. It's safe 174 | // to call it from any goroutine. The AttrGauge should be started before 175 | // calling Inc 176 | func (g *AttrGauge) Inc(attr pdp.AttributeAssignment) { 177 | if g == nil { 178 | return 179 | } 180 | 181 | select { 182 | case g.qChan <- attr: 183 | default: 184 | g.ErrorInc() 185 | } 186 | } 187 | 188 | // synchInc increments internal counter corresponding to the attr. 189 | // The actual prometheus value is not updated in this method. 190 | // Should be called synchronously 191 | func (g *AttrGauge) synchInc(attr pdp.AttributeAssignment) { 192 | ut := g.timeFunc() 193 | id := attr.GetID() 194 | v := serializeOrPanic(attr) 195 | sc := g.perAttr[id][v] 196 | if sc == nil { 197 | sc = NewSlicedCounter(ut) 198 | g.perAttr[id][v] = sc 199 | } 200 | if sc.Inc(ut) { 201 | return 202 | } 203 | g.ErrorInc() 204 | } 205 | 206 | // tick synchronises prometheus gauge with internal counters. 207 | // Should be called synchronously 208 | func (g *AttrGauge) tick() uint32 { 209 | ut := g.timeFunc() 210 | for attr, amap := range g.perAttr { 211 | for val, sc := range amap { 212 | sc.EraseStale(ut) 213 | total := sc.Total() 214 | if total > 0 { 215 | g.pgv.WithLabelValues(attr, val).Set(float64(total)) 216 | continue 217 | } 218 | g.pgv.DeleteLabelValues(attr, val) 219 | delete(amap, val) 220 | } 221 | g.pgv.WithLabelValues(attr, "VALUES_COUNT").Set(float64(len(amap))) 222 | } 223 | return atomic.SwapUint32(&g.errCnt, 0) 224 | } 225 | 226 | // ErrorInc increments error counter 227 | func (g *AttrGauge) ErrorInc() { 228 | atomic.AddUint32(&g.errCnt, 1) 229 | } 230 | 231 | // unixTime returns number of seconds since Unix epoch 232 | func unixTime() uint32 { 233 | return uint32(time.Now().Unix()) 234 | } 235 | 236 | // SetupMetrics checks for configured metrics attributes and starts and 237 | // configures globalAttrGauge as needed 238 | func (pp *ThemisEngine) SetupMetrics(c *caddy.Controller) error { 239 | attrNames := []string{} 240 | for attr, t := range pp.conf.custAttrs { 241 | if !t.isMetrics() { 242 | continue 243 | } 244 | 245 | attrNames = append(attrNames, attr) 246 | 247 | for _, opt := range pp.conf.options { 248 | if opt.name == attr { 249 | opt.metrics = true 250 | } 251 | } 252 | } 253 | if len(attrNames) > 0 { 254 | if mh := dnsserver.GetConfig(c).Handler("prometheus"); mh != nil { 255 | if m, ok := mh.(*metrics.Metrics); ok { 256 | metricsOnce.Do(func() { 257 | m.MustRegister(globalAttrGauge.pgv) 258 | // The globalAttrGauge is started once and is not stopped 259 | // until process termination 260 | globalAttrGauge.Start(DefaultEraseInterval, DefaultQueryChanSize) 261 | }) 262 | globalAttrGauge.AddAttributes(attrNames...) 263 | pp.attrGauges = globalAttrGauge 264 | return nil 265 | } 266 | } 267 | return errors.New("can't find prometheus plugin") 268 | } 269 | return nil 270 | } 271 | 272 | var metricsOnce sync.Once 273 | -------------------------------------------------------------------------------- /plugin/themis/metrics_test.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sync" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | 11 | "github.com/infobloxopen/themis/pdp" 12 | dto "github.com/prometheus/client_model/go" 13 | ) 14 | 15 | // ============= SlicedCounter tests ============== 16 | 17 | func TestSlicedCounterInc(t *testing.T) { 18 | var utime uint32 = 100 19 | sc := NewSlicedCounter(100) 20 | 21 | var wg sync.WaitGroup 22 | wg.Add(10) 23 | for i := 0; i < 10; i++ { 24 | go func() { 25 | for { 26 | ut := atomic.LoadUint32(&utime) 27 | if !sc.Inc(ut) { 28 | if ut < 100+BucketCnt { 29 | t.Errorf("Inc unexpectedly returned false") 30 | } 31 | break 32 | } 33 | if ut >= 100+BucketCnt { 34 | t.Errorf("Inc unexpectedly returned true") 35 | break 36 | } 37 | time.Sleep(time.Nanosecond) 38 | } 39 | wg.Done() 40 | }() 41 | } 42 | for i := 0; i <= BucketCnt; i++ { 43 | atomic.AddUint32(&utime, 1) 44 | time.Sleep(time.Microsecond) 45 | } 46 | wg.Wait() 47 | 48 | checkTotal(t, sc) 49 | } 50 | 51 | func TestNoStale(t *testing.T) { 52 | sc := newTestSlicedCounter(100) 53 | sc.EraseStale(105) 54 | 55 | for i := 0; i < BucketCnt; i++ { 56 | if sc.buckets[i] != uint32(i+10) { 57 | t.Errorf("bucket[%d] unexpectedly was erased", i) 58 | } 59 | } 60 | 61 | checkTotal(t, sc) 62 | } 63 | 64 | func Test6Stale(t *testing.T) { 65 | sc := newTestSlicedCounter(100) 66 | sc.EraseStale(115) 67 | 68 | for i := 0; i < 4; i++ { 69 | if sc.buckets[i] != uint32(i+10) { 70 | t.Errorf("bucket[%d] unexpectedly was erased", i) 71 | } 72 | } 73 | for i := 4; i < 10; i++ { 74 | if sc.buckets[i] != 0 { 75 | t.Errorf("bucket[%d] unexpectedly was not erased", i) 76 | } 77 | } 78 | for i := 10; i < 16; i++ { 79 | if sc.buckets[i] != uint32(i+10) { 80 | t.Errorf("bucket[%d] unexpectedly was erased", i) 81 | } 82 | } 83 | 84 | checkTotal(t, sc) 85 | } 86 | 87 | func TestAllStale(t *testing.T) { 88 | sc := newTestSlicedCounter(100) 89 | sc.EraseStale(130) 90 | 91 | for i := 0; i < BucketCnt; i++ { 92 | if sc.buckets[i] != 0 { 93 | t.Errorf("bucket[%d] unexpectedly was not erased", i) 94 | } 95 | } 96 | 97 | checkTotal(t, sc) 98 | } 99 | 100 | func TestIncVsAllStale(t *testing.T) { 101 | sc := newTestSlicedCounter(100) 102 | var testTime uint32 = 140 103 | var utime uint32 = testTime 104 | 105 | var wg sync.WaitGroup 106 | wg.Add(10) 107 | for i := 0; i < 10; i++ { 108 | go func() { 109 | for { 110 | ut := atomic.LoadUint32(&utime) 111 | sc.Inc(ut) 112 | if ut > testTime { 113 | break 114 | } 115 | time.Sleep(time.Nanosecond) 116 | } 117 | wg.Done() 118 | }() 119 | } 120 | sc.EraseStale(testTime) 121 | 122 | atomic.AddUint32(&utime, 1) 123 | wg.Wait() 124 | 125 | if sc.buckets[utime%BucketCnt] != 10 { 126 | t.Errorf("Unexpected counter after Erase, expected 10, got %d", sc.buckets[utime%BucketCnt]) 127 | } 128 | checkTotal(t, sc) 129 | } 130 | 131 | func TestIncVsPartiallyStale(t *testing.T) { 132 | sc := newTestSlicedCounter(100) 133 | var testTime uint32 = 114 134 | var utime uint32 = testTime 135 | 136 | var wg sync.WaitGroup 137 | wg.Add(10) 138 | for i := 0; i < 10; i++ { 139 | go func() { 140 | for { 141 | ut := atomic.LoadUint32(&utime) 142 | if !sc.Inc(ut) { 143 | t.Errorf("Inc unexpectedly returned false") 144 | } 145 | if ut > testTime { 146 | break 147 | } 148 | time.Sleep(time.Nanosecond) 149 | } 150 | wg.Done() 151 | }() 152 | } 153 | sc.EraseStale(testTime) 154 | 155 | atomic.AddUint32(&utime, 1) 156 | wg.Wait() 157 | 158 | checkTotal(t, sc) 159 | } 160 | 161 | // ============= AttrGauge tests ============== 162 | 163 | func TestStartStop(t *testing.T) { 164 | ag := newTestAttrGauge() 165 | 166 | ag.Start(10*time.Millisecond, 20) 167 | time.Sleep(100 * time.Millisecond) 168 | if atomic.LoadUint32(&ag.state) != AttrGaugeStarted { 169 | t.Errorf("AttrGauge has not started") 170 | } 171 | 172 | ag.Stop() 173 | time.Sleep(100 * time.Millisecond) 174 | if atomic.LoadUint32(&ag.state) != AttrGaugeStopped { 175 | t.Errorf("AttrGauge has not stopped") 176 | } 177 | } 178 | 179 | func TestNegativeStop(t *testing.T) { 180 | ag := newTestAttrGauge() 181 | 182 | ag.state = AttrGaugeStopped 183 | ag.Stop() 184 | if atomic.LoadUint32(&ag.state) == AttrGaugeStopping { 185 | t.Errorf("AttrGauge is unexpectedly stopping") 186 | } 187 | 188 | ag.state = AttrGaugeStopping 189 | ag.Stop() 190 | if atomic.LoadUint32(&ag.state) != AttrGaugeStopping { 191 | t.Errorf("AttrGauge is unexpectedly stopped") 192 | } 193 | } 194 | 195 | func TestAttrGaugeNil(t *testing.T) { 196 | var ag *AttrGauge 197 | 198 | ag.Inc(testAttr()) 199 | ag.Stop() 200 | } 201 | 202 | func TestAttrGaugeInc(t *testing.T) { 203 | ag := newTestAttrGauge() 204 | attr := testAttr() 205 | 206 | setTestTime(100) 207 | ag.synchInc(attr) 208 | ag.tick() 209 | scVal := totalVal(t, ag, attr) 210 | gVal, err := gaugeVal(t, ag, attr) 211 | if err := checkVal(err, gVal, scVal, 1); err != nil { 212 | t.Error(err) 213 | } 214 | 215 | setTestTime(101) 216 | ag.synchInc(attr) 217 | ag.tick() 218 | scVal = totalVal(t, ag, attr) 219 | gVal, err = gaugeVal(t, ag, attr) 220 | if err := checkVal(err, gVal, scVal, 2); err != nil { 221 | t.Error(err) 222 | } 223 | } 224 | 225 | func TestAttrGaugeTick(t *testing.T) { 226 | ag := newTestAttrGauge() 227 | attr := testAttr() 228 | 229 | for i := 100; i < 120; i++ { 230 | setTestTime(uint32(i)) 231 | ag.synchInc(attr) 232 | } 233 | scVal := totalVal(t, ag, attr) 234 | if scVal != 16 { 235 | t.Errorf("unexpected counter, expected %d, got %d", 16, scVal) 236 | } 237 | 238 | setTestTime(120) 239 | eCnt := ag.tick() 240 | if eCnt != 4 { 241 | t.Errorf("unexpected error count, expected %d, got %d", 4, eCnt) 242 | } 243 | scVal = totalVal(t, ag, attr) 244 | gVal, err := gaugeVal(t, ag, attr) 245 | if err := checkVal(err, gVal, scVal, 5); err != nil { 246 | t.Error(err) 247 | } 248 | } 249 | 250 | func TestAttrGaugeEraseValue(t *testing.T) { 251 | ag := newTestAttrGauge() 252 | attr := testAttr() 253 | 254 | setTestTime(100) 255 | ag.Inc(attr) 256 | setTestTime(120) 257 | ag.tick() 258 | scVal := totalVal(t, ag, attr) 259 | gVal, err := gaugeVal(t, ag, attr) 260 | if err := checkVal(err, gVal, scVal, 0); err != nil { 261 | t.Error(err) 262 | } 263 | } 264 | 265 | func TestAttrGaugeSubsequentTicks(t *testing.T) { 266 | ag := newTestAttrGauge() 267 | ag.Start(10*time.Millisecond, 20) 268 | attr := testAttr() 269 | 270 | setTestTime(93) 271 | ag.Inc(attr) 272 | time.Sleep(100 * time.Millisecond) 273 | setTestTime(94) 274 | ag.Inc(attr) 275 | time.Sleep(100 * time.Millisecond) 276 | gVal, err := gaugeVal(t, ag, attr) 277 | if err := checkVal(err, gVal, gVal, 2); err != nil { 278 | t.Error(err) 279 | } 280 | 281 | setTestTime(103) 282 | time.Sleep(100 * time.Millisecond) 283 | gVal, err = gaugeVal(t, ag, attr) 284 | if err := checkVal(err, gVal, gVal, 1); err != nil { 285 | t.Error(err) 286 | } 287 | 288 | setTestTime(104) 289 | time.Sleep(100 * time.Millisecond) 290 | gVal, err = gaugeVal(t, ag, attr) 291 | if err := checkVal(err, gVal, gVal, 0); err != nil { 292 | t.Error(err) 293 | } 294 | 295 | ag.Stop() 296 | } 297 | 298 | func TestAttrGaugeErrorInc(t *testing.T) { 299 | ag := newTestAttrGauge() 300 | attr := testAttr() 301 | 302 | setTestTime(93) 303 | ag.Inc(attr) 304 | ag.Inc(attr) 305 | ag.Inc(attr) 306 | 307 | if ag.errCnt != 3 { 308 | t.Errorf("unexpected error count, expected %d, got %d", 3, ag.errCnt) 309 | } 310 | } 311 | 312 | func TestAttrGaugeAddAttributes(t *testing.T) { 313 | ag := newTestAttrGauge() 314 | setTestTime(100) 315 | 316 | ag.Start(10*time.Millisecond, DefaultQueryChanSize) 317 | // Just make sure the test doesn't panic 318 | 319 | ag.AddAttributes("test_attr1") 320 | ag.Inc(pdp.MakeStringAssignment("test_attr1", "test_value1")) 321 | 322 | ag.AddAttributes("test_attr2") 323 | ag.Inc(pdp.MakeStringAssignment("test_attr2", "test_value2")) 324 | 325 | ag.Stop() 326 | } 327 | 328 | func TestAttrGaugeAddAttributeAgain(t *testing.T) { 329 | ag := newTestAttrGauge() 330 | setTestTime(100) 331 | 332 | ag.Start(10*time.Millisecond, DefaultQueryChanSize) 333 | attr := testAttr() 334 | 335 | ag.Inc(attr) 336 | ag.AddAttributes("test_attr") 337 | ag.Inc(attr) 338 | 339 | time.Sleep(100 * time.Millisecond) 340 | ag.Stop() 341 | gVal, err := gaugeVal(t, ag, attr) 342 | if err = checkVal(err, gVal, gVal, 2); err != nil { 343 | t.Error(err) 344 | } 345 | } 346 | 347 | // ============== utility functions =============== 348 | 349 | func newTestSlicedCounter(ut uint32) *SlicedCounter { 350 | sc := NewSlicedCounter(ut) 351 | for i := 0; i < BucketCnt; i++ { 352 | sc.buckets[i] = uint32(i + 10) 353 | sc.total += sc.buckets[i] 354 | } 355 | return sc 356 | } 357 | 358 | func logSc(t *testing.T, sc *SlicedCounter) { 359 | for i := 0; i < BucketCnt; i++ { 360 | t.Logf("bucket[%d] == %d", i, sc.buckets[i]) 361 | } 362 | } 363 | 364 | func checkTotal(t *testing.T, sc *SlicedCounter) { 365 | var total uint32 366 | for i := 0; i < BucketCnt; i++ { 367 | total += sc.buckets[i] 368 | } 369 | if total != sc.Total() { 370 | t.Errorf("Unexpected total, expected=%d, actual=%d", total, sc.Total()) 371 | } 372 | } 373 | 374 | func testAttr() pdp.AttributeAssignment { 375 | return pdp.MakeStringAssignment("test_attr", "test_value") 376 | } 377 | 378 | var utime uint32 379 | 380 | func testTime() uint32 { 381 | return atomic.LoadUint32(&utime) 382 | } 383 | 384 | func setTestTime(t uint32) { 385 | atomic.StoreUint32(&utime, t) 386 | } 387 | 388 | func newTestAttrGauge() *AttrGauge { 389 | ag := NewAttrGauge() 390 | ag.addAttribute("test_attr") 391 | ag.timeFunc = testTime 392 | return ag 393 | } 394 | 395 | func totalVal(t *testing.T, ag *AttrGauge, attr pdp.AttributeAssignment) uint32 { 396 | if vMap, ok := ag.perAttr[attr.GetID()]; ok { 397 | if sc, ok := vMap[serializeOrPanic(attr)]; ok { 398 | return sc.Total() 399 | } 400 | } 401 | return 0 402 | } 403 | 404 | func gaugeVal(t *testing.T, ag *AttrGauge, attr pdp.AttributeAssignment) (uint32, error) { 405 | g, e := ag.pgv.GetMetricWithLabelValues(attr.GetID(), serializeOrPanic(attr)) 406 | if e != nil { 407 | return 0, e 408 | } 409 | metric := &dto.Metric{} 410 | g.Write(metric) 411 | out, e := json.Marshal(metric) 412 | if e != nil { 413 | return 0, e 414 | } 415 | 416 | result := make(map[string]interface{}) 417 | e = json.Unmarshal(out, &result) 418 | if e != nil { 419 | return 0, e 420 | } 421 | if v, ok := result["gauge"]; ok { 422 | if vMap, ok := v.(map[string]interface{}); ok { 423 | if v, ok := vMap["value"]; ok { 424 | if v, ok := v.(float64); ok { 425 | return uint32(v), nil 426 | } 427 | } 428 | } 429 | } 430 | return 0, fmt.Errorf("Gauge value not found") 431 | } 432 | 433 | func checkVal(err error, gVal, scVal, expVal uint32) error { 434 | if err != nil { 435 | return fmt.Errorf("Failed to get gauge value - %s", err) 436 | } 437 | if gVal != expVal { 438 | return fmt.Errorf("unexpected gauge value, expected %d, got %d", expVal, gVal) 439 | } 440 | if gVal != scVal { 441 | return fmt.Errorf("gauge value mismatch, gauge=%d, slicedCounter=%d", gVal, scVal) 442 | } 443 | return nil 444 | } 445 | 446 | func resetGlobals() { 447 | 448 | } 449 | -------------------------------------------------------------------------------- /plugin/themis/pool.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/infobloxopen/themis/pdp" 7 | ) 8 | 9 | type attrPool struct { 10 | s int 11 | a *sync.Pool 12 | } 13 | 14 | func makeAttrPool(size int, dummy bool) attrPool { 15 | p := attrPool{s: size} 16 | if !dummy { 17 | p.a = &sync.Pool{ 18 | New: func() interface{} { 19 | return p.newAttrs() 20 | }, 21 | } 22 | } 23 | 24 | return p 25 | } 26 | 27 | func (p attrPool) newAttrs() []pdp.AttributeAssignment { 28 | return make([]pdp.AttributeAssignment, p.s) 29 | } 30 | 31 | func (p attrPool) Get() []pdp.AttributeAssignment { 32 | if p.a != nil { 33 | return p.a.Get().([]pdp.AttributeAssignment) 34 | } 35 | 36 | return p.newAttrs() 37 | } 38 | 39 | func (p attrPool) Put(a []pdp.AttributeAssignment) { 40 | if p.a != nil { 41 | p.a.Put(a) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugin/themis/pool_test.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import "testing" 4 | 5 | func TestAttrPool(t *testing.T) { 6 | p := makeAttrPool(10, false) 7 | 8 | a := p.Get() 9 | if len(a) != 10 { 10 | t.Errorf("expected buffer of %d attributes but got %d %#v", 10, len(a), a) 11 | } 12 | 13 | p.Put(a) 14 | } 15 | 16 | func TestDummyAttrPool(t *testing.T) { 17 | p := makeAttrPool(10, true) 18 | 19 | a := p.Get() 20 | if len(a) != 10 { 21 | t.Errorf("expected buffer of %d attributes but got %d %#v", 10, len(a), a) 22 | } 23 | 24 | p.Put(a) 25 | } 26 | -------------------------------------------------------------------------------- /plugin/themis/setup.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "github.com/coredns/caddy" 5 | "github.com/coredns/coredns/core/dnsserver" 6 | "github.com/coredns/coredns/plugin" 7 | ) 8 | 9 | func init() { 10 | caddy.RegisterPlugin(ThemisPluginName, caddy.Plugin{ 11 | ServerType: "dns", 12 | Action: setup, 13 | }) 14 | } 15 | 16 | func setup(c *caddy.Controller) error { 17 | t, err := themisParse(c) 18 | 19 | if err != nil { 20 | return plugin.Error("themis", err) 21 | } 22 | 23 | for _, e := range t.engines { 24 | c.OnStartup(func() error { 25 | e.trace = dnsserver.GetConfig(c).Handler("trace") 26 | err := e.connect() 27 | if err != nil { 28 | return plugin.Error("themis", err) 29 | } 30 | 31 | return e.SetupMetrics(c) 32 | }) 33 | 34 | c.OnShutdown(func() error { 35 | e.closeConn() 36 | return nil 37 | }) 38 | } 39 | 40 | dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { 41 | t.next = next 42 | return t 43 | }) 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /plugin/themis/themis.go: -------------------------------------------------------------------------------- 1 | package themis 2 | 3 | import ( 4 | "errors" 5 | "github.com/coredns/coredns/request" 6 | "github.com/coredns/policy/plugin/firewall/policy" 7 | "github.com/coredns/policy/plugin/pkg/rqdata" 8 | "sync" 9 | 10 | "context" 11 | 12 | "github.com/coredns/coredns/plugin" 13 | "github.com/miekg/dns" 14 | 15 | "github.com/infobloxopen/themis/pdp" 16 | "github.com/infobloxopen/themis/pep" 17 | ) 18 | 19 | var errInvalidAction = errors.New("invalid action") 20 | 21 | const ThemisPluginName = "themis" 22 | 23 | // ThemisPlugin represents a plugin instance that can validate DNS 24 | // requests and replies using PDP server. 25 | 26 | type ThemisEngine struct { 27 | conf config 28 | trace plugin.Handler 29 | next plugin.Handler 30 | pdp pep.Client 31 | attrPool attrPool 32 | attrGauges *AttrGauge 33 | connAttempts map[string]*uint32 34 | unkConnAttempts *uint32 35 | mapping *rqdata.Mapping 36 | wg sync.WaitGroup 37 | } 38 | 39 | func newThemisEngine() *ThemisEngine { 40 | return &ThemisEngine{ 41 | conf: config{ 42 | options: make([]*attrSetting, 0), 43 | custAttrs: make(map[string]custAttr), 44 | connTimeout: -1, 45 | maxReqSize: -1, 46 | maxResAttrs: 64, 47 | }, 48 | connAttempts: make(map[string]*uint32), 49 | unkConnAttempts: new(uint32), 50 | mapping: rqdata.NewMapping(""), 51 | } 52 | } 53 | 54 | func (p *ThemisEngine) BuildQueryData(ctx context.Context, state request.Request) (interface{}, error) { 55 | ah := newAttrHolderWithContext(ctx, rqdata.NewExtractor(state, p.mapping), p.conf.options, p.attrGauges) 56 | return ah, nil 57 | } 58 | 59 | func (p *ThemisEngine) BuildReplyData(ctx context.Context, state request.Request, queryData interface{}) (interface{}, error) { 60 | ah := queryData.(*attrHolder) 61 | ah.prepareResponseFromContext(ctx, rqdata.NewExtractor(state, p.mapping)) 62 | return ah, nil 63 | } 64 | 65 | func (p *ThemisEngine) BuildRule(args []string) (policy.Rule, error) { 66 | return p, nil 67 | } 68 | 69 | func (p *ThemisEngine) Evaluate(data interface{}) (int, error) { 70 | ah := data.(*attrHolder) 71 | var attrsRequest []pdp.AttributeAssignment 72 | if !p.conf.autoResAttrs { 73 | attrsRequest = p.attrPool.Get() 74 | defer p.attrPool.Put(attrsRequest) 75 | } 76 | // validate domain name (validation #1) 77 | if err := p.validate(ah, attrsRequest); err != nil { 78 | return dns.RcodeSuccess, err 79 | } 80 | return int(ah.action), nil 81 | } 82 | 83 | type ThemisPlugin struct { 84 | engines map[string]*ThemisEngine 85 | next plugin.Handler 86 | } 87 | 88 | func newThemisPlugin() *ThemisPlugin { 89 | return &ThemisPlugin{engines: make(map[string]*ThemisEngine)} 90 | } 91 | 92 | // Name implements the Handler interface 93 | func (p *ThemisPlugin) Name() string { return ThemisPluginName } 94 | 95 | // ServeDNS implements the Handler interface. 96 | func (p *ThemisPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { 97 | // do nothing 98 | return plugin.NextOrFailure(p.Name(), p.next, ctx, w, r) 99 | } 100 | 101 | // Engine implements the Engineer interface 102 | func (p *ThemisPlugin) Engine(name string) policy.Engine { 103 | return p.engines[name] 104 | } 105 | --------------------------------------------------------------------------------