| /** @file |
| |
| A brief file description |
| |
| @section license License |
| |
| 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. |
| */ |
| |
| /*************************** -*- Mod: C++ -*- ****************************** |
| P_ActionProcessor.h |
| Created On : 05/02/2017 |
| |
| Description: |
| SNI based Configuration in ATS |
| ****************************************************************************/ |
| #pragma once |
| |
| #include "I_EventSystem.h" |
| #include <vector> |
| #include "P_SSLNextProtocolAccept.h" |
| #include "tscore/ink_inet.h" |
| |
| class ActionItem |
| { |
| public: |
| /** |
| * Context should contain extra data needed to be passed to the actual SNIAction. |
| */ |
| struct Context { |
| /** |
| * if any, fqdn_wildcard_captured_groups will hold the captured groups from the `fqdn` |
| * match which will be used to construct the tunnel destination. |
| */ |
| std::optional<std::vector<std::string>> _fqdn_wildcard_captured_groups; |
| }; |
| |
| virtual int SNIAction(Continuation *cont, const Context &ctx) const = 0; |
| |
| /** |
| This method tests whether this action would have been triggered by a |
| particuarly SNI value and IP address combination. This is run after the |
| TLS exchange finished to see if the client used an SNI name different from |
| the host name to avoid SNI-based policy |
| */ |
| virtual bool |
| TestClientSNIAction(const char *servername, const IpEndpoint &ep, int &policy) const |
| { |
| return false; |
| } |
| virtual ~ActionItem(){}; |
| }; |
| |
| class ControlH2 : public ActionItem |
| { |
| public: |
| ControlH2(bool turn_on) : enable_h2(turn_on) {} |
| ~ControlH2() override {} |
| |
| int |
| SNIAction(Continuation *cont, const Context &ctx) const override |
| { |
| auto ssl_vc = dynamic_cast<SSLNetVConnection *>(cont); |
| if (ssl_vc) { |
| if (!enable_h2) { |
| ssl_vc->disableProtocol(TS_ALPN_PROTOCOL_INDEX_HTTP_2_0); |
| } else { |
| ssl_vc->enableProtocol(TS_ALPN_PROTOCOL_INDEX_HTTP_2_0); |
| } |
| } |
| return SSL_TLSEXT_ERR_OK; |
| } |
| |
| private: |
| bool enable_h2 = false; |
| }; |
| |
| class TunnelDestination : public ActionItem |
| { |
| public: |
| TunnelDestination(const std::string_view &dest, bool decrypt, bool tls_upstream) |
| : destination(dest), tunnel_decrypt(decrypt), tls_upstream(tls_upstream) |
| { |
| need_fix = (destination.find_first_of('$') != std::string::npos); |
| } |
| ~TunnelDestination() override {} |
| |
| int |
| SNIAction(Continuation *cont, const Context &ctx) const override |
| { |
| // Set the netvc option? |
| SSLNetVConnection *ssl_netvc = dynamic_cast<SSLNetVConnection *>(cont); |
| if (ssl_netvc) { |
| // If needed, we will try to amend the tunnel destination. |
| if (ctx._fqdn_wildcard_captured_groups && need_fix) { |
| const auto &fixed_dst = replace_match_groups(destination, *ctx._fqdn_wildcard_captured_groups); |
| ssl_netvc->set_tunnel_destination(fixed_dst, tunnel_decrypt, tls_upstream); |
| Debug("TunnelDestination", "Destination now is [%s], configured [%s]", fixed_dst.c_str(), destination.c_str()); |
| } else { |
| ssl_netvc->set_tunnel_destination(destination, tunnel_decrypt, tls_upstream); |
| } |
| } |
| return SSL_TLSEXT_ERR_OK; |
| } |
| |
| private: |
| bool |
| is_number(const std::string &s) const |
| { |
| return !s.empty() && |
| std::find_if(std::begin(s), std::end(s), [](std::string::value_type c) { return !std::isdigit(c); }) == std::end(s); |
| } |
| |
| /** |
| * `tunnel_route` may contain matching groups ie: `$1` which needs to be replaced by the corresponding |
| * captured group from the `fqdn`, this function will replace them using proper group string. Matching |
| * groups could be at any order. |
| */ |
| std::string |
| replace_match_groups(const std::string &dst, const std::vector<std::string> &groups) const |
| { |
| if (dst.empty() || groups.empty()) { |
| return dst; |
| } |
| std::string real_dst; |
| std::string::size_type pos{0}; |
| |
| const auto end = std::end(dst); |
| // We need to split the tunnel string and place each corresponding match on the |
| // configured one, so we need to first, get the match, then get the match number |
| // making sure that it does exist in the captured group. |
| for (auto c = std::begin(dst); c != end; c++, pos++) { |
| if (*c == '$') { |
| // find the next '.' so we can get the group number. |
| const auto dot = dst.find('.', pos); |
| std::string::size_type to = std::string::npos; |
| if (dot != std::string::npos) { |
| to = dot - (pos + 1); |
| } else { |
| // It may not have a dot, which could be because it's the last part. In that case |
| // we should check for the port separator. |
| if (const auto port = dst.find(':', pos); port != std::string::npos) { |
| to = (port - pos) - 1; |
| } |
| } |
| const auto &number_str = dst.substr(pos + 1, to); |
| if (!is_number(number_str)) { |
| // it may be some issue on the configured string, place the char and keep going. |
| real_dst += *c; |
| continue; |
| } |
| const std::size_t group_index = std::stoi(number_str); |
| if ((group_index - 1) < groups.size()) { |
| // place the captured group. |
| real_dst += groups[group_index - 1]; |
| // if it was the last match, then ... |
| if (dot == std::string::npos && to == std::string::npos) { |
| // that's it. |
| break; |
| } |
| pos += number_str.size() + 1; |
| std::advance(c, number_str.size() + 1); |
| } |
| // If there is no match for a specific group, then we keep the `$#` as defined in the string. |
| } |
| real_dst += *c; |
| } |
| |
| return real_dst; |
| } |
| |
| std::string destination; |
| |
| bool tunnel_decrypt; |
| bool need_fix; |
| bool tls_upstream; |
| }; |
| |
| class VerifyClient : public ActionItem |
| { |
| uint8_t mode; |
| |
| public: |
| VerifyClient(const char *param) : mode(atoi(param)) {} |
| VerifyClient(uint8_t param) : mode(param) {} |
| ~VerifyClient() override {} |
| int |
| SNIAction(Continuation *cont, const Context &ctx) const override |
| { |
| auto ssl_vc = dynamic_cast<SSLNetVConnection *>(cont); |
| Debug("ssl_sni", "action verify param %d", this->mode); |
| setClientCertLevel(ssl_vc->ssl, this->mode); |
| return SSL_TLSEXT_ERR_OK; |
| } |
| bool |
| TestClientSNIAction(const char *servername, const IpEndpoint &ep, int &policy) const override |
| { |
| // This action is triggered by a SNI if it was set |
| return true; |
| } |
| }; |
| |
| class HostSniPolicy : public ActionItem |
| { |
| uint8_t policy; |
| |
| public: |
| HostSniPolicy(const char *param) : policy(atoi(param)) {} |
| HostSniPolicy(uint8_t param) : policy(param) {} |
| ~HostSniPolicy() override {} |
| int |
| SNIAction(Continuation *cont, const Context &ctx) const override |
| { |
| // On action this doesn't do anything |
| return SSL_TLSEXT_ERR_OK; |
| } |
| bool |
| TestClientSNIAction(const char *servername, const IpEndpoint &ep, int &in_policy) const override |
| { |
| // Update the policy when testing |
| in_policy = this->policy; |
| // But this action didn't really trigger during the action phase |
| return false; |
| } |
| }; |
| |
| class TLSValidProtocols : public ActionItem |
| { |
| bool unset = true; |
| unsigned long protocol_mask; |
| |
| public: |
| #ifdef SSL_OP_NO_TLSv1_3 |
| static const unsigned long max_mask = SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2 | SSL_OP_NO_TLSv1_3; |
| #else |
| static const unsigned long max_mask = SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2; |
| #endif |
| TLSValidProtocols() : protocol_mask(max_mask) {} |
| TLSValidProtocols(unsigned long protocols) : unset(false), protocol_mask(protocols) {} |
| int |
| SNIAction(Continuation *cont, const Context &ctx) const override |
| { |
| if (!unset) { |
| auto ssl_vc = dynamic_cast<SSLNetVConnection *>(cont); |
| Debug("ssl_sni", "TLSValidProtocol param 0%x", static_cast<unsigned int>(this->protocol_mask)); |
| ssl_vc->protocol_mask_set = true; |
| ssl_vc->protocol_mask = protocol_mask; |
| } |
| return SSL_TLSEXT_ERR_OK; |
| } |
| }; |
| |
| class SNI_IpAllow : public ActionItem |
| { |
| IpMap ip_map; |
| |
| public: |
| SNI_IpAllow(std::string &ip_allow_list, const std::string &servername) |
| { |
| // the server identified by item.fqdn requires ATS to do IP filtering |
| if (ip_allow_list.length()) { |
| IpAddr addr1; |
| IpAddr addr2; |
| // check format first |
| // check if the input is a comma separated list of IPs |
| ts::TextView content(ip_allow_list); |
| while (!content.empty()) { |
| ts::TextView list{content.take_prefix_at(',')}; |
| if (0 != ats_ip_range_parse(list, addr1, addr2)) { |
| Debug("ssl_sni", "%.*s is not a valid format", static_cast<int>(list.size()), list.data()); |
| break; |
| } else { |
| Debug("ssl_sni", "%.*s added to the ip_allow list %s", static_cast<int>(list.size()), list.data(), servername.c_str()); |
| ip_map.fill(IpEndpoint().assign(addr1), IpEndpoint().assign(addr2), reinterpret_cast<void *>(1)); |
| } |
| } |
| } |
| } // end function SNI_IpAllow |
| |
| int |
| SNIAction(Continuation *cont, const Context &ctx) const override |
| { |
| // i.e, ip filtering is not required |
| if (ip_map.count() == 0) { |
| return SSL_TLSEXT_ERR_OK; |
| } |
| |
| auto ssl_vc = dynamic_cast<SSLNetVConnection *>(cont); |
| auto ip = ssl_vc->get_remote_endpoint(); |
| |
| // check the allowed ips |
| if (ip_map.contains(ip)) { |
| return SSL_TLSEXT_ERR_OK; |
| } else { |
| char buff[256]; |
| ats_ip_ntop(&ip.sa, buff, sizeof(buff)); |
| Debug("ssl_sni", "%s is not allowed. Denying connection", buff); |
| return SSL_TLSEXT_ERR_ALERT_FATAL; |
| } |
| } |
| bool |
| TestClientSNIAction(const char *servrername, const IpEndpoint &ep, int &policy) const override |
| { |
| bool retval = false; |
| if (ip_map.contains(ep)) { |
| retval = true; |
| } |
| return retval; |
| } |
| }; |