Skip to content

Commit cd34871

Browse files
author
Damian Meden
committed
SNI: Add support to match/replace captured group from fqdn into tunnel_route.
1 parent 582df40 commit cd34871

File tree

6 files changed

+191
-26
lines changed

6 files changed

+191
-26
lines changed

doc/admin-guide/files/sni.yaml.en.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ disable_h2 Deprecated for the more general h2 setting. Setting d
110110
to :code:`true` is the same as setting http2 to :code:`on`.
111111

112112
tunnel_route Destination as an FQDN and port, separated by a colon ``:``.
113+
Match group number can be specified by ``$N`` where N should refer to a specified group in
114+
the FQDN, ``tunnel_route: $1.domain``.
113115

114116
This will forward all traffic to the specified destination without first terminating
115117
the incoming TLS connection.
@@ -212,6 +214,21 @@ client certificate.
212214
verify_server_policy: DISABLED
213215
verify_client: STRICT
214216
217+
Use FQDN captured group to match in ``tunnel_route``.
218+
219+
.. code-block:: yaml
220+
221+
sni:
222+
- fqdn: '*.foo.com'
223+
tunnel_route: '$1.myfoo'
224+
- fqdn: '*.bar.*.com'
225+
tunnel_route: '$2.some.$1.yahoo'
226+
227+
FQDN ``some.foo.com`` will match and the captured string will be replaced in the ``tunnel_route`` which will end up being
228+
``some.myfoo``.
229+
Second part is using multiple groups, having ``bob.bar.example.com`` as FQDN, ``tunnel_route`` will end up being
230+
``bar.some.example.yahoo``.
231+
215232
See Also
216233
========
217234

iocore/net/P_SNIActionPerformer.h

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,18 @@
3838
class ActionItem
3939
{
4040
public:
41-
virtual int SNIAction(Continuation *cont) const = 0;
41+
/**
42+
* Context should contain extra data needed to be passed to the actual SNIAction.
43+
*/
44+
struct Context {
45+
/**
46+
* if any, fqdn_wildcard_captured_groups will hold the captured groups from the `fqdn`
47+
* match which will be used to construct the tunnel destination.
48+
*/
49+
std::optional<std::vector<std::string>> _fqdn_wildcard_captured_groups;
50+
};
51+
52+
virtual int SNIAction(Continuation *cont, const Context &ctx) const = 0;
4253

4354
/**
4455
This method tests whether this action would have been triggered by a
@@ -61,7 +72,7 @@ class ControlH2 : public ActionItem
6172
~ControlH2() override {}
6273

6374
int
64-
SNIAction(Continuation *cont) const override
75+
SNIAction(Continuation *cont, const Context &ctx) const override
6576
{
6677
auto ssl_vc = dynamic_cast<SSLNetVConnection *>(cont);
6778
if (ssl_vc) {
@@ -81,21 +92,98 @@ class ControlH2 : public ActionItem
8192
class TunnelDestination : public ActionItem
8293
{
8394
public:
84-
TunnelDestination(const std::string_view &dest, bool decrypt) : destination(dest), tunnel_decrypt(decrypt) {}
95+
TunnelDestination(const std::string_view &dest, bool decrypt) : destination(dest), tunnel_decrypt(decrypt)
96+
{
97+
need_fix = (destination.find_first_of('$') != std::string::npos);
98+
}
8599
~TunnelDestination() override {}
86100

87101
int
88-
SNIAction(Continuation *cont) const override
102+
SNIAction(Continuation *cont, const Context &ctx) const override
89103
{
90104
// Set the netvc option?
91105
SSLNetVConnection *ssl_netvc = dynamic_cast<SSLNetVConnection *>(cont);
92106
if (ssl_netvc) {
93-
ssl_netvc->set_tunnel_destination(destination, tunnel_decrypt);
107+
// If needed, we will try to amend the tunnel destination.
108+
if (ctx._fqdn_wildcard_captured_groups && need_fix) {
109+
const auto &fixed_dst = replace_match_groups(destination, *ctx._fqdn_wildcard_captured_groups);
110+
ssl_netvc->set_tunnel_destination(fixed_dst, tunnel_decrypt);
111+
Debug("TunnelDestination", "Destination now is [%s], configured [%s]", fixed_dst.c_str(), destination.c_str());
112+
} else {
113+
ssl_netvc->set_tunnel_destination(destination, tunnel_decrypt);
114+
}
94115
}
95116
return SSL_TLSEXT_ERR_OK;
96117
}
97118
std::string destination;
98119
bool tunnel_decrypt = false;
120+
121+
private:
122+
bool
123+
is_number(const std::string &s) const
124+
{
125+
return !s.empty() &&
126+
std::find_if(std::begin(s), std::end(s), [](std::string::value_type c) { return !std::isdigit(c); }) == std::end(s);
127+
}
128+
129+
/**
130+
* `tunnel_route` may contain matching groups ie: `$1` which needs to be replaced by the corresponding
131+
* captured group from the `fqdn`, this function will replace them using proper group string. Matching
132+
* groups could be at any order.
133+
*/
134+
std::string
135+
replace_match_groups(const std::string &dst, const std::vector<std::string> &groups) const
136+
{
137+
if (dst.empty() || groups.empty()) {
138+
return dst;
139+
}
140+
std::string real_dst;
141+
std::string::size_type pos{0};
142+
143+
const auto end = std::end(dst);
144+
// We need to split the tunnel string and place each corresponding match on the
145+
// configured one, so we need to first, get the match, then get the match number
146+
// making sure that it does exist in the captured group.
147+
for (auto c = std::begin(dst); c != end; c++, pos++) {
148+
if (*c == '$') {
149+
// find the next '.' so we can get the group number.
150+
const auto dot = dst.find('.', pos);
151+
std::string::size_type to = std::string::npos;
152+
if (dot != std::string::npos) {
153+
to = dot - (pos + 1);
154+
} else {
155+
// It may not have a dot, which could be because it's the last part. In that case
156+
// we should check for the port separator.
157+
if (const auto port = dst.find(':', pos); port != std::string::npos) {
158+
to = (port - pos) - 1;
159+
}
160+
}
161+
const auto &number_str = dst.substr(pos + 1, to);
162+
if (!is_number(number_str)) {
163+
// it may be some issue on the configured string, place the char and keep going.
164+
real_dst += *c;
165+
continue;
166+
}
167+
const std::size_t group_index = std::stoi(number_str);
168+
if ((group_index - 1) < groups.size()) {
169+
// place the captured group.
170+
real_dst += groups[group_index - 1];
171+
// if it was the last match, then ...
172+
if (dot == std::string::npos && to == std::string::npos) {
173+
// that's it.
174+
break;
175+
}
176+
pos += number_str.size() + 1;
177+
std::advance(c, number_str.size() + 1);
178+
}
179+
}
180+
real_dst += *c;
181+
}
182+
183+
return real_dst;
184+
}
185+
186+
bool need_fix;
99187
};
100188

101189
class VerifyClient : public ActionItem
@@ -107,7 +195,7 @@ class VerifyClient : public ActionItem
107195
VerifyClient(uint8_t param) : mode(param) {}
108196
~VerifyClient() override {}
109197
int
110-
SNIAction(Continuation *cont) const override
198+
SNIAction(Continuation *cont, const Context &ctx) const override
111199
{
112200
auto ssl_vc = dynamic_cast<SSLNetVConnection *>(cont);
113201
Debug("ssl_sni", "action verify param %d", this->mode);
@@ -131,7 +219,7 @@ class HostSniPolicy : public ActionItem
131219
HostSniPolicy(uint8_t param) : policy(param) {}
132220
~HostSniPolicy() override {}
133221
int
134-
SNIAction(Continuation *cont) const override
222+
SNIAction(Continuation *cont, const Context &ctx) const override
135223
{
136224
// On action this doesn't do anything
137225
return SSL_TLSEXT_ERR_OK;
@@ -160,7 +248,7 @@ class TLSValidProtocols : public ActionItem
160248
TLSValidProtocols() : protocol_mask(max_mask) {}
161249
TLSValidProtocols(unsigned long protocols) : unset(false), protocol_mask(protocols) {}
162250
int
163-
SNIAction(Continuation *cont) const override
251+
SNIAction(Continuation *cont, const Context &ctx) const override
164252
{
165253
if (!unset) {
166254
auto ssl_vc = dynamic_cast<SSLNetVConnection *>(cont);
@@ -205,7 +293,7 @@ class SNI_IpAllow : public ActionItem
205293
} // end function SNI_IpAllow
206294

207295
int
208-
SNIAction(Continuation *cont) const override
296+
SNIAction(Continuation *cont, const Context &ctx) const override
209297
{
210298
// i.e, ip filtering is not required
211299
if (ip_map.count() == 0) {

iocore/net/P_SSLSNI.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ struct namedElement {
6666
}
6767
pos = 0;
6868
while ((pos = name.find('*', pos)) != std::string::npos) {
69-
name.replace(pos, 1, ".{0,}");
69+
name.replace(pos, 1, "(.{0,})");
7070
}
7171
Debug("ssl_sni", "Regexed fqdn=%s", name.c_str());
7272
setRegexName(name);
@@ -111,7 +111,7 @@ struct SNIConfigParams : public ConfigInfo {
111111
void cleanup();
112112
int Initialize();
113113
void loadSNIConfig();
114-
const actionVector *get(const std::string &servername) const;
114+
std::pair<const actionVector *, ActionItem::Context> get(const std::string &servername) const;
115115
};
116116

117117
struct SNIConfig {

iocore/net/SSLSNIConfig.cc

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
#include <pcre.h>
4141

4242
static ConfigUpdateHandler<SNIConfig> *sniConfigUpdate;
43+
static constexpr int OVECSIZE{30};
4344

4445
const NextHopProperty *
4546
SNIConfigParams::getPropertyConfig(const std::string &servername) const
@@ -108,20 +109,42 @@ int SNIConfig::configid = 0;
108109
/*definition of member functions of SNIConfigParams*/
109110
SNIConfigParams::SNIConfigParams() {}
110111

111-
const actionVector *
112+
std::pair<const actionVector *, ActionItem::Context>
112113
SNIConfigParams::get(const std::string &servername) const
113114
{
114-
int ovector[2];
115+
int ovector[OVECSIZE];
116+
ActionItem::Context context;
117+
115118
for (const auto &retval : sni_action_list) {
116119
int length = servername.length();
117120
if (retval.match == nullptr && length == 0) {
118-
return &retval.actions;
119-
} else if (pcre_exec(retval.match, nullptr, servername.c_str(), length, 0, 0, ovector, 2) == 1 && ovector[0] == 0 &&
120-
ovector[1] == length) {
121-
return &retval.actions;
121+
return {&retval.actions, context};
122+
} else if (auto offset = pcre_exec(retval.match, nullptr, servername.c_str(), length, 0, 0, ovector, OVECSIZE); offset >= 0) {
123+
if (offset == 1 && ovector[1] == length) {
124+
// full match
125+
return {&retval.actions, context};
126+
}
127+
// If contains groups
128+
if (offset == 0) {
129+
// reset to max if too many.
130+
offset = OVECSIZE / 3;
131+
}
132+
133+
const char *psubStrMatchStr = nullptr;
134+
std::vector<std::string> groups;
135+
for (int strnum = 1; strnum < offset; strnum++) {
136+
pcre_get_substring(servername.c_str(), ovector, offset, strnum, &(psubStrMatchStr));
137+
groups.emplace_back(psubStrMatchStr);
138+
}
139+
context._fqdn_wildcard_captured_groups = std::move(groups);
140+
if (psubStrMatchStr) {
141+
pcre_free_substring(psubStrMatchStr);
142+
}
143+
144+
return {&retval.actions, context};
122145
}
123146
}
124-
return nullptr;
147+
return {nullptr, context};
125148
}
126149

127150
int
@@ -197,9 +220,10 @@ SNIConfig::TestClientAction(const char *servername, const IpEndpoint &ep, int &h
197220
{
198221
bool retval = false;
199222
SNIConfig::scoped_config params;
200-
const actionVector *actionvec = params->get(servername);
201-
if (actionvec) {
202-
for (auto &&item : *actionvec) {
223+
224+
const auto &actions = params->get(servername);
225+
if (actions.first) {
226+
for (auto &&item : *actions.first) {
203227
retval |= item->TestClientSNIAction(servername, ep, host_sni_policy);
204228
}
205229
}

iocore/net/SSLUtils.cc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,12 +376,11 @@ static int
376376
PerformAction(Continuation *cont, const char *servername)
377377
{
378378
SNIConfig::scoped_config params;
379-
const actionVector *actionvec = params->get(servername);
380-
if (!actionvec) {
379+
if (const auto &actions = params->get(servername); !actions.first) {
381380
Debug("ssl_sni", "%s not available in the map", servername);
382381
} else {
383-
for (auto &&item : *actionvec) {
384-
auto ret = item->SNIAction(cont);
382+
for (auto &&item : *actions.first) {
383+
auto ret = item->SNIAction(cont, actions.second);
385384
if (ret != SSL_TLSEXT_ERR_OK) {
386385
return ret;
387386
}

tests/gold_tests/tls/tls_tunnel.test.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
server_foo = Test.MakeOriginServer("server_foo", ssl=True)
2727
server_bar = Test.MakeOriginServer("server_bar", ssl=True)
2828
server2 = Test.MakeOriginServer("server2")
29+
#dns = Test.MakeDNServer("dns", default=['127.0.0.1'])
30+
dns = Test.MakeDNServer("dns")
2931

3032
request_foo_header = {"headers": "GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
3133
request_bar_header = {"headers": "GET / HTTP/1.1\r\nHost: bar.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
@@ -44,6 +46,9 @@
4446
ts.addSSLfile("ssl/signer.pem")
4547
ts.addSSLfile("ssl/signer.key")
4648

49+
dns.addRecords(records={"localhost": ["127.0.0.1"]})
50+
dns.addRecords(records={"one.testmatch": ["127.0.0.1"]})
51+
dns.addRecords(records={"two.example.one": ["127.0.0.1"]})
4752
# Need no remap rules. Everything should be proccessed by sni
4853

4954
# Make sure the TS server certs are different from the origin certs
@@ -61,7 +66,9 @@
6166
'proxy.config.ssl.client.CA.cert.path': '{0}'.format(ts.Variables.SSLDir),
6267
'proxy.config.ssl.client.CA.cert.filename': 'signer.pem',
6368
'proxy.config.exec_thread.autoconfig.scale': 1.0,
64-
'proxy.config.url_remap.pristine_host_hdr': 1
69+
'proxy.config.url_remap.pristine_host_hdr': 1,
70+
'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port),
71+
'proxy.config.dns.resolv_conf': 'NULL'
6572
})
6673

6774
# foo.com should not terminate. Just tunnel to server_foo
@@ -73,6 +80,10 @@
7380
" tunnel_route: localhost:{0}".format(server_foo.Variables.SSL_Port),
7481
"- fqdn: bob.*.com",
7582
" tunnel_route: localhost:{0}".format(server_foo.Variables.SSL_Port),
83+
"- fqdn: '*.match.com'",
84+
" tunnel_route: $1.testmatch:{0}".format(server_foo.Variables.SSL_Port),
85+
"- fqdn: '*.ok.*.com'",
86+
" tunnel_route: $2.example.$1:{0}".format(server_foo.Variables.SSL_Port),
7687
"- fqdn: ''", # No SNI sent
7788
" tunnel_route: localhost:{0}".format(server_bar.Variables.SSL_Port)
7889
])
@@ -82,6 +93,7 @@
8293
tr.ReturnCode = 0
8394
tr.Processes.Default.StartBefore(server_foo)
8495
tr.Processes.Default.StartBefore(server_bar)
96+
tr.Processes.Default.StartBefore(dns)
8597
tr.Processes.Default.StartBefore(Test.Processes.ts)
8698
tr.StillRunningAfter = ts
8799
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded")
@@ -120,6 +132,31 @@
120132
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server")
121133
tr.Processes.Default.Streams.All += Testers.ContainsExpression("bar ok", "Should get a response from bar")
122134

135+
136+
tr = Test.AddTestRun("one.match.com Tunnel-test")
137+
tr.Processes.Default.Command = "curl -vvv --resolve 'one.match.com:{0}:127.0.0.1' -k https://one.match.com:{0}".format(ts.Variables.ssl_port)
138+
tr.ReturnCode = 0
139+
tr.StillRunningAfter = ts
140+
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded")
141+
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Not Found on Accelerato", "Should not try to remap on Traffic Server")
142+
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server")
143+
tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response")
144+
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server")
145+
tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from tm")
146+
147+
148+
tr = Test.AddTestRun("one.ok.two.com Tunnel-test")
149+
tr.Processes.Default.Command = "curl -vvv --resolve 'one.ok.two.com:{0}:127.0.0.1' -k https:/one.ok.two.com:{0}".format(ts.Variables.ssl_port)
150+
tr.ReturnCode = 0
151+
tr.StillRunningAfter = ts
152+
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Could Not Connect", "Curl attempt should have succeeded")
153+
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("Not Found on Accelerato", "Should not try to remap on Traffic Server")
154+
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("CN=foo.com", "Should not TLS terminate on Traffic Server")
155+
tr.Processes.Default.Streams.All += Testers.ContainsExpression("HTTP/1.1 200 OK", "Should get a successful response")
156+
tr.Processes.Default.Streams.All += Testers.ExcludesExpression("ATS", "Do not terminate on Traffic Server")
157+
tr.Processes.Default.Streams.All += Testers.ContainsExpression("foo ok", "Should get a response from tm")
158+
159+
123160
# Update sni file and reload
124161
tr = Test.AddTestRun("Update config files")
125162
# Update the SNI config

0 commit comments

Comments
 (0)