diff --git a/CMakeLists.txt b/CMakeLists.txt index 49e861b679a..b464fcbea67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ include_directories( include tests/include lib + lib/yamlcpp/include proxy proxy/hdrs proxy/http diff --git a/ci/rat-regex.txt b/ci/rat-regex.txt index 32b0f3f4c2e..1c9a0760174 100644 --- a/ci/rat-regex.txt +++ b/ci/rat-regex.txt @@ -68,3 +68,4 @@ port\.h ^catch[.]hpp$ ^configuru.hpp$ ^yamlcpp$ +^tests/gold_tests/autest-site/min_cfg$ diff --git a/configs/Makefile.am b/configs/Makefile.am index ccd4983b4a9..0f60e0cdfff 100644 --- a/configs/Makefile.am +++ b/configs/Makefile.am @@ -29,7 +29,7 @@ nodist_sysconf_DATA = \ dist_sysconf_DATA = \ cache.config.default \ hosting.config.default \ - ip_allow.config.default \ + ip_allow.yaml.default \ logging.yaml.default \ parent.config.default \ plugin.config.default \ diff --git a/configs/ip_allow.config.default b/configs/ip_allow.config.default deleted file mode 100644 index 264d0786d3d..00000000000 --- a/configs/ip_allow.config.default +++ /dev/null @@ -1,27 +0,0 @@ -# -# ip_allow.config -# -# Documentation: -# https://docs.trafficserver.apache.org/en/latest/admin-guide/files/ip_allow.config.en.html -# -# Rules: -# src_ip= action= [method=] -# -# Actions: ip_allow, ip_deny -# -# Multiple method keywords can be specified (method=GET method=HEAD), or -# multiple methods can be separated by an '|' (method=GET|HEAD). The method -# keyword is optional and it is defaulted to ALL. -# Available methods: ALL, GET, CONNECT, DELETE, HEAD, OPTIONS, -# POST, PURGE, PUT, TRACE, PUSH -# -# Rules are applied in the order listed starting from the top. -# That means you generally want to append your rules after the ones listed here. -# -# Allow anything on localhost (this is the default configuration based on the -# deprecated CONFIG proxy.config.http.quick_filter.mask INT 0x482) -src_ip=127.0.0.1 action=ip_allow method=ALL -src_ip=::1 action=ip_allow method=ALL -# Deny PURGE, DELETE, and PUSH for all (this implies allow other methods for all) -src_ip=0.0.0.0-255.255.255.255 action=ip_deny method=PUSH|PURGE|DELETE -src_ip=::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff action=ip_deny method=PUSH|PURGE|DELETE diff --git a/configs/ip_allow.schema.json b/configs/ip_allow.schema.json new file mode 100644 index 00000000000..ba495b6d8c2 --- /dev/null +++ b/configs/ip_allow.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://github.com/apache/trafficserver/tree/master/configs/ip_allow.schema.json", + "title": "Traffic Server IP Allow Configuration", + "description": "IP ACL configuration file structure. Licensed under Apache V2 https://www.apache.org/licenses/LICENSE-2.0", + "type": "object", + "properties": { + "version": { + "type": "string", + "description": "Configuration format version." + }, + "ip_addr_acl": { + "description": "Root tag for IP address ACL configuration", + "type": "array", + "items": { + "$ref": "#/definitions/rule" + } + } + }, + "required": [ "ip_addr_acl" ], + "definitions": { + "range": { + "description": "A range of IP addresses in a single family.", + "type": "string" + }, + "action": { + "description": "Enforcement action.", + "type": "string", + "enum": [ "allow", "deny" ] + }, + "methods": { + "description": "Methods to check.", + "oneOf": [ + { + "type": "string", + "description": "Method name" + }, + { + "type": "array", + "description": "List of method names.", + "minItems": 1, + "items": { + "type": "string", + "description": "Method name" + } + } + ] + }, + "rule": { + "description": "Connection ACL.", + "type": "object", + "properties": { + "apply": { + "description": "Where to apply the rule, inbound or outbound.", + "type": "string", + "enum": [ "in", "out" ] + }, + "ip_addrs": { + "oneOf": [ + { + "$ref": "#/definitions/range" + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/range" + } + } + ] + }, + "action": { + "$ref": "#/definitions/action" + }, + "methods": { + "$ref": "#/definitions/methods" + } + }, + "required": [ "apply", "ip_addrs", "action" ] + } + } +} diff --git a/configs/ip_allow.yaml.default b/configs/ip_allow.yaml.default new file mode 100644 index 00000000000..35b872ba83e --- /dev/null +++ b/configs/ip_allow.yaml.default @@ -0,0 +1,50 @@ +# YAML +# +# ip_allow.config +# +# Documentation: +# https://docs.trafficserver.apache.org/en/latest/admin-guide/files/ip_allow.config.en.html +# +# Rules: +# Each rule is a mapping, with the tags +# +# apply: Either "in" or "out" to apply to inbound and outbound connections respectively. +# ip_addrs: IP address ranges, either a single range or a list of ranges. +# action: "allow" or "deny" +# methods: A method name or sequence of method names. Available methods: GET, CONNECT, DELETE, +# HEAD, OPTIONS, POST, PURGE, PUT, TRACE, PUSH. The special name "ALL" indicates all +# methods and it overrides any other methods. +# +# A rule must have either "src" or "dst" to indicate if the IP addresses apply to inbound connections +# or outbound connections. +# +# The top level tag 'ip_addr_acl' identifies the rule items. Its value must be a rule item or a +# sequence of rule items. +# +# Rules are applied in the order listed starting from the top. +# That means you generally want to append your rules after the ones listed here. +# +# Allow anything on localhost, limit destructive methods elsewhere. +ip_addr_acl: + - apply: in + ip_addrs: 127.0.0.1 + action: allow + methods: ALL + - apply: in + ip_addrs: ::1 + action: allow + methods: ALL + - apply: in + ip_addrs: 0/0 + action: deny + methods: + - PURGE + - PUSH + - DELETE + - apply: in + ip_addrs: ::/0 + action: deny + methods: + - PURGE + - PUSH + - DELETE diff --git a/doc/admin-guide/files/ip_allow.config.en.rst b/doc/admin-guide/files/ip_allow.config.en.rst index 8a43a725cf6..185af87cd0a 100644 --- a/doc/admin-guide/files/ip_allow.config.en.rst +++ b/doc/admin-guide/files/ip_allow.config.en.rst @@ -1,134 +1,225 @@ -.. Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at +.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license + agreements. See the NOTICE file distributed with this work for additional information regarding + copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance with the License. You may obtain a + copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. + Unless required by applicable law or agreed to in writing, software distributed under the License + is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + or implied. See the License for the specific language governing permissions and limitations under + the License. .. include:: ../../common.defs -.. highlight:: none +.. highlight:: yaml =============== -ip_allow.config +ip_allow.yaml =============== -.. configfile:: ip_allow.config +.. configfile:: ip_allow.yaml -The :file:`ip_allow.config` file controls client access to |TS| and |TS| connections to the servers. -You can specify ranges of IP addresses that are allowed to connect to |TS| or that are allowed to be -remapped by Traffic Server. After you modify the :file:`ip_allow.config` file, navigate to the |TS| -bin directory and run the :option:`traffic_ctl config reload` command to apply changes. +The :file:`ip_allow.yaml` file controls client access to |TS| and |TS| connections to upstream servers. +This control is specified rules. Each rule has + +* A direction (inbound or out). +* A range of IP address to which the rule applies. +* An action, either accept or deny. +* A list of HTTP methods. + +Inbound rules control access to |TS| from user agents. Outbound rules control access to upstream destinations +from |TS|. The IP addresses always apply to the remote address for |TS|. That is, the user agent IP address +for inbound rules and the upstream destination address for outbound rules. The rule can apply at the connection +level or just to specific methods. + +|TS| can be updated for changes to the rules in :file:`ip_allow.yaml` file, by running the +:option:`traffic_ctl config reload`. Format ====== -Each line in :file:`ip_allow.config` file must have on of the following formats -format:: - - src_ip= action= [method=] - dest_ip= action= [method=] - -For ``src_ip`` the remote inbound connection address, i.e. the IP address of the client, is checked -against the specified range of IP addresses. For ``dest_ip`` the outbound remote address (i.e. the IP -address to which |TS| connects) is checked against the specified IP address range. - -Range specifications can be IPv4 or IPv6, but any single range must be one or the other. Ranges can -be specified by two addresses, the lower address and the upper address, separated by a dash, ``-``. -Such a range inclusive and contains the lower, upper addresses and all addresses in between. A range -can also be specified by an address and a CIDR mask, separated by a slash, ``/``. This case is -converted to a range of the previous case by retaining only the left most ``mask`` bits, clearing -the rest for the lower address and setting them for the upper address. For instance, a mask of -``23`` would mean the left most 23 bits are kept and all bits to the right are cleared or set. -Finally, a range can be a single IP address which matches exactly that address (the equivalent of a -range with the lower and upper values equal to that IP address). - -The value of ``method`` is a string which must consist of either HTTP method names separated by the -character '|' or the keyword literal ``ALL``. This keyword may omitted in which case it is treated -as if it were ``method=ALL``. Methods can also be specified by having multiple instances of the -``method`` keyword, each specifying a single method. E.g., ``method=GET|HEAD`` is the same as -``method=GET method=HEAD``. The method names are not validated which means non-standard method names -can be specified. - -The ``action`` must be either ``ip_allow`` or ``ip_deny``. This controls what |TS| does if the -address is in the range and the method matches. If there is a match, |TS| allows the connection (for -``ip_allow``) or denies it (``ip_deny``). - -For each inbound or outbound connection the applicable rule is selected by first match on the IP -address. The rule is then applied (if the method matches) or its opposite is applied (if the method -doesn't match). If no rule is matched access is allowed. This makes each rule both an accept and -deny, one explicit and the other implicit. The ``src_ip`` rules are checked when a host connects -to |TS|. The ``dest_ip`` rules are checked when |TS| connects to another host. - -By default the :file:`ip_allow.config` file contains the following lines, which allows all methods -to connections from localhost and denies the ``PUSH``, ``PURGE`` and ``DELETE`` methods to all other -IP addresses (note this allows all other methods to all IP addresses):: - - src_ip=127.0.0.1 action=ip_allow method=ALL - src_ip=::1 action=ip_allow method=ALL - src_ip=0.0.0.0-255.255.255.255 action=ip_deny method=PUSH|PURGE|DELETE - src_ip=::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff action=ip_deny method=PUSH|PURGE|DELETE - -This could also be specified as:: - - src_ip=127.0.0.1 action=ip_allow method=ALL - src_ip=::1 action=ip_allow method=ALL - src_ip=0/0 action=ip_deny method=PUSH|PURGE|DELETE - src_ip=::/0 action=ip_deny method=PUSH|PURGE|DELETE +:file:`ip_allow.yaml` is YAML format. The default configuration is:: + + # YAML + ip_addr_acl: + - apply: in + ip_addrs: 127.0.0.1 + action: allow + methods: ALL + - apply: in + ip_addrs: ::1 + action: allow + methods: ALL + - apply: in + ip_addrs: 0/0 + action: deny + methods: + - PURGE + - PUSH + - DELETE + - apply: in + ip_addrs: ::/0 + action: deny + methods: + - PURGE + - PUSH + - DELETE + +Each rule is a mapping. The YAML data must have a top level key of "ip_addr_acl" and its value must +be a mapping or a sequence of mappings, each of those being one rule. + +The keys in a rule are + +``apply`` + This is where the rule is applied, either ``in`` or ``out``. Inbound application means + the rule is applied to inbound user agent connections. Outbound application means the rule is + applied to outbound connections from |TS| to an upstream destination. This is a required key. + +``ip_addrs`` + IP addresses to match for the rule to be applied. This can be either an address range or an + array of address ranges. This is a required key. + +``action`` + The action, which must be ``allow`` or ``deny``. This is a required key. + +``methods`` + This is optional. If not present, the rule action applies to all methods. If present, the rule + action is applied connections using those methods and its opposite to all other connections. The + keyword "ALL" means all methods, making the specification of any other method redundant. All + methods comparisons are case insensitive. This is an optional key. + +An IP address range can be specified in several ways. A range is always IPv4 or IPv6, it is not +allowed to have a range that contains addresses from different IP address families. + +* A single address, which specifies a range of size 1, e.g. "127.0.0.1". +* A minimum and maximum address separated by a dash, e.g. "10.1.0.0-10.1.255.255". +* A CIDR based value, e.g. "10.1.0.0/16", which is a range containing exactly the specified network. + +A rule must have the ``apply``, ``ip_addrs``, and ``action`` keys. Rules match based on +IP addresses only, and are then applied to all matching sessions. If the rule is an ``allow`` rule, +the specified methods are allowed and all other methods are denied. If the rule is a ``deny`` rule, +the specified methods are denied and all other methods are allowed. + +For example, from the default configuration, the rule for ``127.0.0.1`` is ``allow`` with all +methods. Therefore an inbound connection from the loopback address (127.0.0.1) is allowed to use any +method. The general IPv4 rule, covering all IPv4 address, is a ``deny`` rule and therefore when it +matches the methods "PURGE", "PUSH", and "DELETE" are denied, any other method is allowed. + +The rules are matched in order, by IP address, therefore the general IPv4 rule does not apply to the +loopback address because the latter is matched first. + +A major difference in application between ``in`` and ``out`` rules is that by default, +inbound connections are denied and therefore if there is no rule that matches, the connection is +denied. Outbound rules allow by default, so the absence of rules in the default configuration +enables all methods for all outbound connections. Examples ======== The following example enables all clients access.:: - src_ip=0.0.0.0-255.255.255.255 action=ip_allow + apply: in + ip_addrs: 0.0.0.0-255.255.255.255 + action: allow The following example allows access to all clients on addresses in a subnet:: - src_ip=123.12.3.000-123.12.3.123 action=ip_allow + apply: in + ip_addrs: 123.12.3.000-123.12.3.123 + action: allow The following example denies access all clients on addresses in a subnet:: - src_ip=123.45.6.0-123.45.6.123 action=ip_deny + apppy: in + ip_addrs: 123.45.6.0-123.45.6.123 + action: deny If the entire subnet were to be denied, that would be:: - src_ip=123.45.6.0/24 action=ip_deny + apply: in + ip_addrs: 123.45.6.0/24 + action: deny -The following example allows one to any upstream servers:: +The following example allows any method to any upstream servers:: - dest_ip=0.0.0.0-255.255.255.255 action=ip_allow + apply: out + ip_addrs: 0.0.0.0-255.255.255.255 + action: allow Alternatively this can be done with:: - dest_ip=0/0 action=ip_allow + apply: out + ip_addrs: 0/0 + action: allow + +Or also by having no rules at all, as outbound by default is allow. The following example denies to access all servers on a specific subnet:: - dest_ip=10.0.0.0-10.0.255.255 action=ip_deny + apply: out + ip_addr: 10.0.0.0-10.0.255.255 + action: deny Alternatively:: - dest_ip=10.0.0.0/16 action=ip_deny + apply: out + ip_addrs: 10.0.0.0/16 + action: deny + +The ``ip_addrs`` can be an array of ranges, so that:: + + - apply: in + ip_addrs: 10.0.0.0/8 + action: deny + - apply: in + ip_addrs: 172.16.0.0/20 + action: deny + - apply: in + ip_addrs: 192.168.1.0/24 + action: deny + +can be done more simply as:: + + apply: in + ip_addrs: + - 10.0.0.0/8 + - 172.16.0.0/20 + - 192.168.1.0/24 + action: deny If the goal is to allow only ``GET`` and ``HEAD`` requests to those servers, it would be:: - dest_ip=10.0.0.0/16 action=ip_allow method=GET method=HEAD + apply: out + ip_addrs: 10.0.0.0/16 + methods: [ GET, HEAD ] + action: allow -or:: +Alternatively:: - dest_ip=10.0.0.0/16 action=ip_allow method=GET|HEAD + apply: out + ip_addrs: 10.0.0.0/16 + methods: + - GET + - HEAD + action: allow This will match the IP address for the target servers on the outbound connection. Then, if the method is ``GET`` or ``HEAD`` the connection will be allowed, otherwise the connection will be denied. +As a final example, here is the default configuration in compact form:: + + ip_addr_acl: [ + { apply: in, ip_addrs: 127.0.0.1, action: allow }, + { apply: in, ip_addrs: "::1", action: allow }, + { apply: in, ip_addrs: 0/0, action: deny, methods: [ PURGE, PUSH, DELETE ] }, + { apply: in, ip_addrs: "::/0", action: deny, methods: [ PURGE, PUSH, DELTE ] } + ] + +.. note:: + + For ATS 9.0, this file is (almost) backwards compatible. If the first line is a single '#' + character, or contains only "# ats", then the file will be read in the version 8.0 format. This + is true for the default format and so if that has not been changed it should still work. This + allows a grace period before ATS 10.0 which will drop the old format entirely. diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst index 11107d13e40..439fba4ea58 100644 --- a/doc/admin-guide/files/records.config.en.rst +++ b/doc/admin-guide/files/records.config.en.rst @@ -1749,7 +1749,7 @@ Security .. important:: If you enable this option, then you must also specify - a filtering rule in the ip_allow.config file to allow only certain + a filtering rule in the ip_allow.yaml file to allow only certain machines to push content into the cache. .. ts:cv:: CONFIG proxy.config.http.max_post_size INT 0 diff --git a/doc/admin-guide/files/remap.config.en.rst b/doc/admin-guide/files/remap.config.en.rst index 29562265e0e..a4b2f83b596 100644 --- a/doc/admin-guide/files/remap.config.en.rst +++ b/doc/admin-guide/files/remap.config.en.rst @@ -416,7 +416,7 @@ Acl Filters =========== Acl filters can be created to control access of specific remap lines. The markup -is very similar to that of :file:`ip_allow.config`, with slight changes to +is very similar to that of :file:`ip_allow.yaml`, with slight changes to accommodate remap markup Examples diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc index 9d4a2add0f3..d728bf9dde1 100644 --- a/mgmt/RecordsConfig.cc +++ b/mgmt/RecordsConfig.cc @@ -792,7 +792,7 @@ static const RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.cache.control.filename", RECD_STRING, "cache.config", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , - {RECT_CONFIG, "proxy.config.cache.ip_allow.filename", RECD_STRING, "ip_allow.config", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} + {RECT_CONFIG, "proxy.config.cache.ip_allow.filename", RECD_STRING, "ip_allow.yaml", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , {RECT_CONFIG, "proxy.config.cache.hosting_filename", RECD_STRING, "hosting.config", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , diff --git a/proxy/IPAllow.cc b/proxy/IPAllow.cc index 8adadb7a3e4..1761a0c7e54 100644 --- a/proxy/IPAllow.cc +++ b/proxy/IPAllow.cc @@ -27,10 +27,13 @@ #include #include "IPAllow.h" #include "tscore/BufferWriter.h" +#include "tscore/ts_file.h" +#include "tscore/ink_memory.h" -extern char *readIntoBuffer(const char *file_path, const char *module_name, int *read_size_ptr); +#include "yaml-cpp/yaml.h" using ts::TextView; + namespace { void @@ -42,8 +45,64 @@ SignalError(ts::BufferWriter &w, bool &flag) } Error("%s", w.data()); } + +template +void +ParseError(ts::TextView fmt, Args &&... args) +{ + ts::LocalBufferWriter<1024> w; + w.printv(fmt, std::forward_as_tuple(args...)); + w.write('\0'); + Warning("%s", w.data()); +} + } // namespace +namespace ts +{ +BufferWriter & +bwformat(BufferWriter &w, BWFSpec const &spec, IpAllow const *obj) +{ + return w.print("{}[{}]", obj->MODULE_NAME, obj->get_config_file().c_str()); +} + +BufferWriter & +bwformat(BufferWriter &w, BWFSpec const &spec, YAML::Mark const &mark) +{ + return w.print("Line {}", mark.line); +} + +BufferWriter & +bwformat(BufferWriter &w, BWFSpec const &spec, std::error_code const &ec) +{ + return w.print("[{}:{}]", ec.value(), ec.message()); +} + +} // namespace ts + +namespace YAML +{ +template <> struct convert { + static Node + encode(ts::TextView const &tv) + { + Node zret; + zret = std::string(tv.data(), tv.size()); + return zret; + } + static bool + decode(const Node &node, ts::TextView &tv) + { + if (!node.IsScalar()) { + return false; + } + tv.assign(node.Scalar()); + return true; + } +}; + +} // namespace YAML + enum AclOp { ACL_OP_ALLOW, ///< Allow access. ACL_OP_DENY, ///< Deny access. @@ -77,14 +136,14 @@ IpAllow::reconfigure() { self_type *new_table; - Note("ip_allow.config loading ..."); + Note("ip_allow.yaml loading ..."); new_table = new self_type("proxy.config.cache.ip_allow.filename"); new_table->BuildTable(); configid = configProcessor.set(configid, new_table); - Note("ip_allow.config finished loading"); + Note("ip_allow.yaml finished loading"); } IpAllow * @@ -133,7 +192,7 @@ IpAllow::match(sockaddr const *ip, match_key_t key) // End API functions // -IpAllow::IpAllow(const char *config_var) : config_file_path(RecConfigReadConfigPath(config_var)) {} +IpAllow::IpAllow(const char *config_var) : config_file(ats_scoped_str(RecConfigReadConfigPath(config_var)).get()) {} void IpAllow::PrintMap(IpMap *map) @@ -194,27 +253,222 @@ IpAllow::Print() int IpAllow::BuildTable() { - int file_size = 0; - int line_num = 0; - IpAddr addr1; - IpAddr addr2; - bool alarmAlready = false; - ts::LocalBufferWriter<1024> bw_err; - // Table should be empty ink_assert(_src_map.count() == 0 && _dst_map.count() == 0); - file_buff = readIntoBuffer(config_file_path, "ip-allow", &file_size); + std::error_code ec; + std::string content{ts::file::load(config_file, ec)}; + if (ec.value() == 0) { + // If it's a .yaml or the root tag is present, treat as YAML. + if (TextView{config_file.view()}.take_suffix_at('.') == "yaml" || std::string::npos != content.find(YAML_TAG_ROOT)) { + this->YAMLBuildTable(content); + } else { + this->ATSBuildTable(content); + } - if (file_buff == nullptr) { - Warning("%s Failed to read %s. All IP Addresses will be blocked", MODULE_NAME, config_file_path.get()); + if (_src_map.count() == 0 && _dst_map.count() == 0) { + ParseError("{} - No entries found. All IP Addresses will be blocked", this); + return 1; + } + // convert the coloring from indices to pointers. + for (auto &item : _src_map) { + item.setData(&_src_acls[reinterpret_cast(item.data())]); + } + for (auto &item : _dst_map) { + item.setData(&_dst_acls[reinterpret_cast(item.data())]); + } + if (is_debug_tag_set("ip-allow")) { + Print(); + } + } else { + ParseError("{} Failed to load {}. All IP Addresses will be blocked", this, ec); return 1; } + return 0; +} + +bool +IpAllow::YAMLLoadMethod(const YAML::Node &node, Record &rec) +{ + const std::string &value{node.Scalar()}; + + if (0 == strcasecmp(value, YAML_VALUE_METHODS_ALL)) { + rec._method_mask = ALL_METHOD_MASK; + } else { + int method_idx = hdrtoken_tokenize(value.data(), value.size()); + if (method_idx < HTTP_WKSIDX_CONNECT || method_idx >= HTTP_WKSIDX_CONNECT + HTTP_WKSIDX_METHODS_CNT) { + rec._nonstandard_methods.push_back(value); + Debug("ip-allow", "Found nonstandard method '%s' at line %d", value.c_str(), node.Mark().line); + } else { // valid method. + rec._method_mask |= ACL::MethodIdxToMask(method_idx); + } + } + return true; +} - TextView src(file_buff, file_size); +bool +IpAllow::YAMLLoadIPAddrRange(const YAML::Node &node, IpMap *map, void *mark) +{ + if (node.IsScalar()) { + IpAddr min, max; + if (0 == ats_ip_range_parse(node.Scalar(), min, max)) { + map->fill(min, max, mark); + return true; + } else { + ParseError("{} {} - '{}' is not a valid range.", this, node.Mark(), node.Scalar()); + } + } + return false; +} + +bool +IpAllow::YAMLLoadEntry(const YAML::Node &entry) +{ + AclOp op = ACL_OP_DENY; // "shut up", I explained to the compiler. + YAML::Node node; + IpAddr min, max; + std::string value; + Record rec; + std::vector *acls{nullptr}; + IpMap *map = nullptr; + + if (!entry.IsMap()) { + ParseError("{} {} - ACL items must be maps.", this, entry.Mark()); + return false; + } + + if (entry[YAML_TAG_APPLY]) { + auto apply_node{entry[YAML_TAG_APPLY]}; + if (apply_node.IsScalar()) { + ts::TextView value{apply_node.Scalar()}; + if (0 == strcasecmp(value, YAML_VALUE_APPLY_IN)) { + acls = &_src_acls; + map = &_src_map; + } else if (0 == strcasecmp(value, YAML_VALUE_APPLY_OUT)) { + acls = &_dst_acls; + map = &_dst_map; + } else { + ParseError(R"("{}" value at {} must be "{}" or "{}")", YAML_TAG_APPLY, entry.Mark(), YAML_VALUE_APPLY_IN, + YAML_VALUE_APPLY_OUT); + return false; + } + } else { + ParseError(R"("{}" value at {} must be a scalar, "{}" or "{}")", YAML_TAG_APPLY, entry.Mark(), YAML_VALUE_APPLY_IN, + YAML_VALUE_APPLY_OUT); + return false; + } + } else { + ParseError(R"("Object at {} must have a "{}" key.)", entry.Mark(), YAML_TAG_APPLY); + return false; + } + + void *ipmap_mark = reinterpret_cast(acls->size()); + if (entry[YAML_TAG_IP_ADDRS]) { + auto addr_node{entry[YAML_TAG_IP_ADDRS]}; + if (addr_node.IsSequence()) { + for (auto const &n : addr_node) { + if (!this->YAMLLoadIPAddrRange(n, map, ipmap_mark)) { + return false; + } + } + } else if (!this->YAMLLoadIPAddrRange(addr_node, map, ipmap_mark)) { + return false; + } + } + + if (!entry[YAML_TAG_ACTION]) { + ParseError("{} {} - item ignored, required '{}' key not found.", this, entry.Mark(), YAML_TAG_ACTION); + return false; + } + + node = entry[YAML_TAG_ACTION]; + if (!node.IsScalar()) { + ParseError("{} {} - item ignored, value for tag '{}' must be a string", this, node.Mark(), YAML_TAG_ACTION); + return false; + } + value = node.as(); + if (value == YAML_VALUE_ACTION_ALLOW) { + op = ACL_OP_ALLOW; + } else if (value == YAML_VALUE_ACTION_DENY) { + op = ACL_OP_DENY; + } else { + ParseError("{} {} - item ignored, value for tag '{}' must be '{}' or '{}'", this, node.Mark(), YAML_TAG_ACTION, + YAML_VALUE_ACTION_ALLOW, YAML_VALUE_ACTION_DENY); + return false; + } + if (!entry[YAML_TAG_METHODS]) { + rec._method_mask = ALL_METHOD_MASK; + } else { + node = entry[YAML_TAG_METHODS]; + if (node.IsScalar()) { + this->YAMLLoadMethod(node, rec); + } else if (node.IsSequence()) { + for (auto const &elt : node) { + if (elt.IsScalar()) { + this->YAMLLoadMethod(elt, rec); + if (rec._method_mask == ALL_METHOD_MASK) { + break; // we're done here, nothing else matters. + } + } else { + ParseError("{} {} - item ignored, all values for '{}' must be strings.", this, elt.Mark(), YAML_TAG_METHODS); + return false; + } + } + } else { + ParseError("{} {} - item ignored, value for '{}' must be a single string or a list of strings.", this, node.Mark(), + YAML_TAG_METHODS); + } + } + if (op == ACL_OP_DENY) { + rec._method_mask = ALL_METHOD_MASK & ~rec._method_mask; + rec._deny_nonstandard_methods = true; + } + rec._src_line = entry.Mark().line; + // If we get here, everything parsed OK, add the record. + acls->emplace_back(std::move(rec)); + return true; +} + +int +IpAllow::YAMLBuildTable(std::string const &content) +{ + YAML::Node root{YAML::Load(content)}; + if (!root.IsMap()) { + ParseError("{} - top level object was not a map. All IP Addresses will be blocked", this); + return 1; + } + + YAML::Node data{root[YAML_TAG_ROOT]}; + if (!data) { + ParseError("{} - root tag '{}' not found. All IP Addresses will be blocked", this, YAML_TAG_ROOT); + } else if (data.IsSequence()) { + for (auto const &entry : data) { + if (!this->YAMLLoadEntry(entry)) { + return 1; + } + } + } else if (data.IsMap()) { + this->YAMLLoadEntry(data); // singleton, just load it. + } else { + ParseError("{} - root tag '{}' is not an map or sequence. All IP Addresses will be blocked", this, YAML_TAG_ROOT); + return 1; + } + return 0; +} + +int +IpAllow::ATSBuildTable(std::string const &content) +{ + int line_num = 0; + IpAddr addr1; + IpAddr addr2; + bool alarmAlready = false; + ts::LocalBufferWriter<1024> bw_err; + + TextView src(content); TextView line; auto err_prefix = [&]() -> ts::BufferWriter & { - return bw_err.reset().print("{} discarding '{}' entry at line {} : ", MODULE_NAME, config_file_path, line_num); + return bw_err.reset().print("{} discarding '{}' entry at line {} : ", MODULE_NAME, config_file.c_str(), line_num); }; while (!(line = src.take_prefix_at('\n')).empty()) { @@ -277,7 +531,7 @@ IpAllow::BuildTable() } else { int method_idx = hdrtoken_tokenize(method_name.data(), method_name.size()); if (method_idx < HTTP_WKSIDX_CONNECT || method_idx >= HTTP_WKSIDX_CONNECT + HTTP_WKSIDX_METHODS_CNT) { - nonstandard_methods.push_back(method_name); + nonstandard_methods.emplace_back(std::string(method_name.data(), method_name.size())); Debug("ip-allow", "%s", bw_err.reset().print("Found nonstandard method '{}' on line {}\0", method_name, line_num).data()); } else { // valid method. @@ -329,21 +583,5 @@ IpAllow::BuildTable() } } } - - if (_src_map.count() == 0 && _dst_map.count() == 0) { - Warning("%s No entries in %s. All IP Addresses will be blocked", MODULE_NAME, config_file_path.get()); - } else { - // convert the coloring from indices to pointers. - for (auto &item : _src_map) { - item.setData(&_src_acls[reinterpret_cast(item.data())]); - } - for (auto &item : _dst_map) { - item.setData(&_dst_acls[reinterpret_cast(item.data())]); - } - } - - if (is_debug_tag_set("ip-allow")) { - Print(); - } return 0; } diff --git a/proxy/IPAllow.h b/proxy/IPAllow.h index f36edabc42f..a7dbb88b363 100644 --- a/proxy/IPAllow.h +++ b/proxy/IPAllow.h @@ -32,17 +32,20 @@ #include #include -#include #include -#include #include "hdrs/HTTP.h" #include "ProxyConfig.h" #include "tscore/IpMap.h" #include "tscpp/util/TextView.h" +#include "tscore/ts_file.h" // forward declare in name only so it can be a friend. struct IpAllowUpdate; +namespace YAML +{ +class Node; +} /** Singleton class for access controls. */ @@ -50,10 +53,7 @@ class IpAllow : public ConfigInfo { friend struct IpAllowUpdate; - // These point in to the bulk loaded configuration file, which therefore needs to be kept around - // until the configuration is destructed. The number is expected to be small enough a vector is the - // best container. - using MethodNames = std::vector; + using MethodNames = std::vector; static constexpr uint32_t ALL_METHOD_MASK = ~0; // Mask for all methods. @@ -63,7 +63,8 @@ class IpAllow : public ConfigInfo struct Record { /// Default constructor. /// Present only to make Vec<> happy, do not use. - Record() = default; + Record() = default; + Record(Record &&that) = default; explicit Record(uint32_t method_mask); Record(uint32_t method_mask, int line, MethodNames &&nonstandard_methods, bool deny_nonstandard_methods); @@ -88,6 +89,19 @@ class IpAllow : public ConfigInfo static constexpr ts::TextView OPT_METHOD{"method"}; static constexpr ts::TextView OPT_METHOD_ALL{"all"}; + static constexpr ts::TextView YAML_TAG_ROOT{"ip_addr_acl"}; + static constexpr ts::TextView YAML_TAG_IP_ADDRS{"ip_addrs"}; + static constexpr ts::TextView YAML_TAG_APPLY{"apply"}; + static constexpr ts::TextView YAML_VALUE_APPLY_IN{"in"}; + static constexpr ts::TextView YAML_VALUE_APPLY_OUT{"out"}; + static constexpr ts::TextView YAML_TAG_ACTION{"action"}; + static constexpr ts::TextView YAML_VALUE_ACTION_ALLOW{"allow"}; + static constexpr ts::TextView YAML_VALUE_ACTION_DENY{"deny"}; + static constexpr ts::TextView YAML_TAG_METHODS{"methods"}; + static constexpr ts::TextView YAML_VALUE_METHODS_ALL{"all"}; + + static constexpr const char *MODULE_NAME = "IPAllow"; + /** An access control record and support data. * The primary point of this is to hold the backing configuration in memory while the ACL * is in use. @@ -166,21 +180,22 @@ class IpAllow : public ConfigInfo */ static bool isAcceptCheckEnabled(); + const ts::file::path &get_config_file() const; + private: static size_t configid; ///< Configuration ID for update management. static const Record ALLOW_ALL_RECORD; ///< Static record that allows all access. static bool accept_check_p; ///< @c true if deny all can be enforced during accept. - static constexpr const char *MODULE_NAME = "IPAllow"; - void PrintMap(IpMap *map); int BuildTable(); + int ATSBuildTable(const std::string &); + int YAMLBuildTable(const std::string &); + bool YAMLLoadEntry(const YAML::Node &); + bool YAMLLoadIPAddrRange(const YAML::Node &, IpMap *map, void *mark); + bool YAMLLoadMethod(const YAML::Node &node, Record &rec); - ats_scoped_str config_file_path; ///< Path to configuration file. - /// The file contents so records can point in to this instead of separately allocating. - ats_scoped_str file_buff; - // const char *module_name{nullptr}; - // const char *action{nullptr}; + ts::file::path config_file; ///< Path to configuration file. IpMap _src_map; IpMap _dst_map; std::vector _src_acls; @@ -315,3 +330,9 @@ IpAllow::makeAllowAllACL() -> ACL { return {&ALLOW_ALL_RECORD, nullptr}; } + +inline const ts::file::path & +IpAllow::get_config_file() const +{ + return config_file; +} diff --git a/proxy/Makefile.am b/proxy/Makefile.am index 30afed79735..2d7d5f37a46 100644 --- a/proxy/Makefile.am +++ b/proxy/Makefile.am @@ -32,6 +32,7 @@ AM_CPPFLAGS += \ -I$(abs_srcdir)/hdrs \ -I$(abs_top_srcdir)/mgmt \ -I$(abs_top_srcdir)/mgmt/utils \ + -I$(abs_top_srcdir)/lib/yamlcpp/include \ $(TS_INCLUDES) noinst_HEADERS = \ diff --git a/src/traffic_manager/AddConfigFilesHere.cc b/src/traffic_manager/AddConfigFilesHere.cc index 30be5a6ab4a..859bb70952a 100644 --- a/src/traffic_manager/AddConfigFilesHere.cc +++ b/src/traffic_manager/AddConfigFilesHere.cc @@ -76,7 +76,7 @@ initializeRegistry() registerFile("proxy.config.socks.socks_config_file", "socks.config"); registerFile("records.config", "records.config"); registerFile("proxy.config.cache.control.filename", "cache.config"); - registerFile("proxy.config.cache.ip_allow.filename", "ip_allow.config"); + registerFile("proxy.config.cache.ip_allow.filename", "ip_allow.yaml"); registerFile("proxy.config.http.parent_proxy.file", "parent.config"); registerFile("proxy.config.url_remap.filename", "remap.config"); registerFile("", "volume.config"); diff --git a/tests/gold_tests/autest-site/min_cfg/ip_allow.config b/tests/gold_tests/autest-site/min_cfg/ip_allow.config deleted file mode 100644 index 061bbe5c0b6..00000000000 --- a/tests/gold_tests/autest-site/min_cfg/ip_allow.config +++ /dev/null @@ -1,4 +0,0 @@ -src_ip=127.0.0.1 action=ip_allow method=ALL -src_ip=::1 action=ip_allow method=ALL -src_ip=0.0.0.0-255.255.255.255 action=ip_deny method=PUSH|PURGE|DELETE -src_ip=::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff action=ip_deny method=PUSH|PURGE|DELETE \ No newline at end of file diff --git a/tests/gold_tests/autest-site/min_cfg/ip_allow.yaml b/tests/gold_tests/autest-site/min_cfg/ip_allow.yaml new file mode 100644 index 00000000000..9f80a0b4d07 --- /dev/null +++ b/tests/gold_tests/autest-site/min_cfg/ip_allow.yaml @@ -0,0 +1,43 @@ +# YAML + +# Licensed to the Apache Software Foundation (ASF) under one or more contributor license +# agreements. See the NOTICE file distributed with this work for additional information regarding +# copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License +# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +# or implied. See the License for the specific language governing permissions and limitations under +# the License. + +# Allow anything on localhost, limit destructive methods elsewhere. +ip_addr_acl: + - apply: in + ip_addrs: 127.0.0.1 + action: allow + methods: ALL + - apply: out + ip_addrs: [ 10.0.0.0/8, 192.168.1.0/24 ] + action: allow + methods: [GET, HEAD, POST ] + - apply: in + ip_addrs: ::1 + action: allow + methods: ALL + - apply: in + ip_addrs: 0/0 + action: deny + methods: + - PURGE + - PUSH + - DELETE + - apply: in + ip_addrs: ::/0 + action: deny + methods: + - PURGE + - PUSH + - DELETE diff --git a/tests/gold_tests/autest-site/trafficserver.test.ext b/tests/gold_tests/autest-site/trafficserver.test.ext index 9f2a4bb984e..0ca41eb12a6 100755 --- a/tests/gold_tests/autest-site/trafficserver.test.ext +++ b/tests/gold_tests/autest-site/trafficserver.test.ext @@ -214,7 +214,7 @@ def MakeATSProcess(obj, name, command='traffic_server', select_ports=True, enabl tmpname = os.path.join(config_dir, fname) p.Disk.File(tmpname, id=make_id(fname), typename="ats:config") - fname = "ip_allow.config" + fname = "ip_allow.yaml" tmpname = os.path.join(config_dir, fname) p.Disk.File(tmpname, id=make_id(fname), typename="ats:config")