Skip to content

SNOW-3969787: Send SNI when probing endpoints whose host contains an underscore - #2729

Open
sfc-gh-ckoch wants to merge 6 commits into
masterfrom
ckoch/SNOW-3969787-sni-underscore-host
Open

sfc-gh-ckoch wants to merge 6 commits into
masterfrom
ckoch/SNOW-3969787-sni-underscore-host

Conversation

@sfc-gh-ckoch

@sfc-gh-ckoch sfc-gh-ckoch commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Overview

SNOW-3969787

CertificateDiagnosticCheck built its https:// probe URL from the raw allowlist host. An underscore is not a Letter-Digit-Hyphen character, so SNIHostName rejects such a host and the JDK silently completes the handshake with no server_name extension.

So the check reported on whatever default certificate the endpoint serves for an SNI-less handshake, not the one a real client gets — the opposite of what a connectivity diagnostic is for.

Snowflake also serves the account name with each underscore replaced by a hyphen, so the check now probes that. The substitution lives in one place, SnowflakeUtil.normalizeSnowflakeHost, shared with HttpAndHttpsDiagnosticCheck (which had its own unscoped replace('_', '-')) and SnowflakeConnectString.

It only applies to hosts containing .snowflakecomputing. — only Snowflake guarantees the hyphenated variant, so third-party hosts are probed as listed. allowUnderscoresInHost still opts out, and now does so for the diagnostics too.

Notes

  • Normalization now requires .snowflakecomputing., and hyphenates the whole host rather than only the account prefix. Neither is reachable for a real Snowflake host. The host.startsWith(account) guard is unchanged.
  • Third-party hosts in allowlist.json are hyphenated: testRunDiagnosticContextMethods opens real connections to every host in that file, and duosecurity.com wildcard-resolves, so CI was emitting a real SNI-less handshake every run. The underscored Snowflake hosts stay.

Pre-review self checklist

  • PR branch is updated with all the changes from master branch
  • The code is correctly formatted (run mvn -P check-style validate)
  • New public API is not unnecessary exposed (run mvn verify and inspect target/japicmp/japicmp.html)
  • The pull request name is prefixed with SNOW-XXXX:
  • Code is in compliance with internal logging requirements
  1. Please describe how your code solves the related issue.

    normalizeSnowflakeHost returns the hyphenated form for Snowflake hosts and leaves everything else alone; each call site gates it on allowUnderscoresInHost. Covered by SnowflakeUtilTest, ConnectStringParseTest (both flag states, account-prefix guard) and DiagnosticContextTest.

@sfc-gh-ckoch
sfc-gh-ckoch requested a review from a team as a code owner August 18, 2026 18:00
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@sfc-gh-ckoch

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@sfc-gh-ckoch

Copy link
Copy Markdown
Contributor Author

recheck

* @param hostname host as listed in the allowlist file
* @return a host suitable for SNI, or the original host if no valid variant exists
*/
static String toSniCompatibleHost(String hostname) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this check run only against snowflake domain names or also for some third parties? If it applies to third parties, the rewrite there may not work as expected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CertificateDiagnostic appears to run against third parties, so I constrained the rewrite to snowflakecomputing, but changed some of the text fixture URLs to be RFC compliant so our tests never make an SNI-less connection.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped to Snowflake hosts, so third-party entries are probed exactly as listed. In practice an underscored third-party host should not arise: the allowlist comes from SYSTEM$ALLOWLIST() and real third-party entries are real DNS names, with underscores only ever in the account identifier. See the allowlist.json thread for why our test fixture was the exception.

@sfc-gh-rkowalski sfc-gh-rkowalski left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things I'd like resolved before merge:

  1. The PR description doesn't match the code. It describes toSniCompatibleHost, reusing SNIHostName as a validator, a warning on the no-valid-variant path, and a CertificateDiagnosticCheckTest but none of which are in the diff. The CHANGELOG is accurate but description is outdated.
  2. ALLOW_UNDERSCORES_IN_HOST is silently neutered. After this change, SnowflakeConnectString no longer reads the flag, so setAllowUnderscoresInHost(true) becomes inert for *.snowflakecomputing.* hosts. That flag was added deliberately (#703, SNOW-544896 regionless-URL support) as an escape hatch to preserve underscore hosts, and its default was re-tuned in #1189.. Either honor it (see inline suggestion) or make its removal an explicit as this will probably be a BCR.

Comment thread src/main/java/net/snowflake/client/internal/jdbc/SnowflakeConnectString.java Outdated
Comment thread src/test/resources/allowlist.json
@sfc-gh-ckoch
sfc-gh-ckoch force-pushed the ckoch/SNOW-3969787-sni-underscore-host branch from fe9d37a to 856d319 Compare August 26, 2026 00:38
@sfc-gh-ckoch

Copy link
Copy Markdown
Contributor Author

One behavior change worth calling out that nobody asked about: the old code only rewrote when host.startsWith(account); I had dropped that guard and it is now restored, since the host need not contain the account at all. Two narrowings remain on purpose — normalization requires .snowflakecomputing., and all underscores in a Snowflake host are hyphenated rather than just the account prefix. Neither is reachable for a real Snowflake host.

@sfc-gh-ckoch
sfc-gh-ckoch force-pushed the ckoch/SNOW-3969787-sni-underscore-host branch from 856d319 to 21b050c Compare August 31, 2026 18:29

@sfc-gh-rkowalski sfc-gh-rkowalski left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both earlier change requests are resolved: the description now matches the code, and ALLOW_UNDERSCORES_IN_HOST is a live opt-out again in both the connect string and the diagnostic checks. Implementation is clean and well-tested. Approving; one non-blocking consistency note inline.

// variant that Snowflake also serves for account names containing underscores.
String hostname = snowflakeEndpoint.getHost();
if (!this.proxyConf.isAllowUnderscoresInHost()) {
hostname = SnowflakeUtil.normalizeSnowflakeHost(hostname);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this normalization is applied here and in HttpAndHttpsDiagnosticCheck, but DnsDiagnosticCheck and TcpDiagnosticCheck still probe snowflakeEndpoint.getHost() raw. So for an underscored Snowflake account a single diagnostic run splits: TLS/HTTP probe the hyphenated host the client actually connects to, while DNS/TCP probe the underscored name (legal as a DNS label, but not the name a real client uses). Not in the SNI scope of this PR, but either normalize there too or add a line to the PR notes stating DNS/TCP intentionally stay on the literal allowlist host. Fine to merge either way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had decided not to change the Tcp or Dns check because they don't exercise the TLS path. Given that we are only normalizing Snowflake hostnames and this assumption is baked all throughout our client/driver code base, I think it should be safe like this.

sfc-gh-ckoch and others added 6 commits September 8, 2026 04:57
…underscore

CertificateDiagnosticCheck built its https:// probe URL from the raw
allowlist host. Snowflake account names may contain underscores, and an
underscore is not a Letter-Digit-Hyphen character, so SNIHostName rejects
such a host and the JDK silently completes the handshake with no
server_name extension at all.

Two consequences. The check reports on whatever default certificate the
endpoint serves for an SNI-less handshake rather than the certificate a
real client would be presented, which is the opposite of what a
connectivity diagnostic is for. It also emits handshakes that stand out
from every other client on the wire, since the driver's normal Apache
HttpClient path does send SNI.

Reuse SNIHostName itself as the validator so this cannot drift from the
rule the JDK actually applies, and fall back to the hyphenated form of
the host, which Snowflake also serves for account names containing
underscores. Hosts that are already valid are probed exactly as listed.
When no valid variant exists the original host is kept and the SNI-less
handshake is logged as a warning instead of happening silently, so the
endpoint the allowlist asked for is still the one probed.

Note that HttpAndHttpsDiagnosticCheck already applied the same
underscore-to-hyphen substitution; this brings the certificate check in
line with it.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
Replace the three separate underscore-to-hyphen host implementations
(CertificateDiagnosticCheck.toSniCompatibleHost, the blind replace in
HttpAndHttpsDiagnosticCheck, and the ALLOW_UNDERSCORES_IN_HOST flag logic
in SnowflakeConnectString) with a single SnowflakeUtil.normalizeSnowflakeHost
helper. The helper is scoped to Snowflake hosts, so third-party allowlist
hosts (cloud storage, OCSP responders, Duo, ...) are left unchanged instead
of being blindly rewritten. Drops the dead ALLOW_UNDERSCORES_IN_HOST flag.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
The Snowsight and Duo hosts in the diagnostic test fixtures used
underscores, which are not valid in hostnames (RFC 952 Letter-Digit-Hyphen).
Real third-party hosts never contain underscores, so replace them with
hyphens in allowlist.json, DiagnosticContextLatestIT (kept in lockstep for
the containsAll assertion), and the SnowflakeUtilTest third-party cases.
Third-party URLs are still never rewritten by normalizeSnowflakeHost, which
is scoped to Snowflake hosts.

.... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code)

Co-Authored-By: Cortex Code <noreply@snowflake.com>
…uard

The unification dropped two behaviors that the previous per-call-site
implementations had. Restore both.

allowUnderscoresInHost is not a dead flag: it is the opt-out for
deployments whose DNS only resolves the underscored name, which PrivateLink
customers rely on. Removing the read made the public
setAllowUnderscoresInHost(true) a silent no-op, a behavior change for
anyone who had set it. It is honoured again for connect strings, and now
also for the diagnostic checks - a customer who set it is telling us their
DNS only resolves the underscored host, so diagnosing the hyphenated
variant would report on an endpoint they never connect to. The flag rides
on ProxyConfig, which DiagnosticContext already builds from the connection
property map and hands to every check.

normalizeSnowflakeHost stays a pure function rather than taking the flag as
a parameter: SnowflakeUtil is static with no session in scope, so callers
own the decision.

The host is again only rewritten when it starts with the account
identifier. The host need not contain the account at all - it may be an IP
address, or a name that merely routes to the account - and rewriting one we
cannot tie to the account points the driver at a name Snowflake never
promised to serve. The guard belongs only on the connect-string path, where
host and account come from the same URL; in the diagnostics they are
independent inputs and the allowlist's underscored entry is the regionless
org-form host, which frequently shares no prefix with the connection's
account.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…derscores

DiagnosticContextLatestIT.testRunDiagnosticContextMethods is the only test
that calls runDiagnostics(), and it does so against
src/test/resources/allowlist.json - opening real DNS, TCP, TLS and HTTP
connections to every host that file lists.

Third-party hosts are deliberately never rewritten, because only Snowflake
serves an equivalent hyphenated name, so an underscored one is probed as
listed and the JDK completes the handshake with no server_name extension.
duosecurity.com resolves through a wildcard, so CI emitted a genuine
SNI-less handshake to a third party on every run; this was visible as DNS
queries for duo_security.duosecurity.com in VPC flow logs. Those names are
also fiction - a real SYSTEM$ALLOWLIST carries underscores only in the
Snowflake account identifier.

Record that reasoning on the test that makes the connections, since JSON
cannot carry a comment, and leave a pointer where mockEndpoints mirrors the
fixture for the containsAll assertion.

The Snowflake hosts in the fixture keep their underscores: they are what
exercises the normalization, and they are probed as the hyphenated variant
that carries SNI. Coverage that third-party hosts are left alone moves to
SnowflakeUtilTest, where it is a string assertion that opens no sockets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sfc-gh-ckoch
sfc-gh-ckoch force-pushed the ckoch/SNOW-3969787-sni-underscore-host branch from 21b050c to e4a4045 Compare September 8, 2026 04:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants