Skip to content

Commit 8a1e3d8

Browse files
authored
Merge pull request #351 from DeterminateSystems/sync-2.33.3
Sync with upstream 2.33.3
2 parents c964271 + 1a84409 commit 8a1e3d8

File tree

11 files changed

+595
-36
lines changed

11 files changed

+595
-36
lines changed

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.33.2
1+
2.33.3

doc/manual/source/release-notes/rl-2.33.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,35 @@ This release was made possible by the following 33 contributors:
279279
- Henry [**(@cootshk)**](https://github.com/cootshk)
280280
- Martin Joerg [**(@mjoerg)**](https://github.com/mjoerg)
281281
- Farid Zakaria [**(@fzakaria)**](https://github.com/fzakaria)
282+
# Release 2.33.3 (2026-02-13)
283+
284+
- S3 binary caches now use virtual-hosted-style addressing by default [#15208](https://github.com/NixOS/nix/issues/15208)
285+
286+
S3 binary caches now use virtual-hosted-style URLs
287+
(`https://bucket.s3.region.amazonaws.com/key`) instead of path-style URLs
288+
(`https://s3.region.amazonaws.com/bucket/key`) when connecting to standard AWS
289+
S3 endpoints. This enables HTTP/2 multiplexing and fixes TCP connection
290+
exhaustion (TIME_WAIT socket accumulation) under high-concurrency workloads.
291+
292+
A new `addressing-style` store option controls this behavior:
293+
294+
- `auto` (default): virtual-hosted-style for standard AWS endpoints, path-style
295+
for custom endpoints.
296+
- `path`: forces path-style addressing (deprecated by AWS).
297+
- `virtual`: forces virtual-hosted-style addressing (bucket names must not
298+
contain dots).
299+
300+
Bucket names containing dots (e.g., `my.bucket.name`) automatically fall back
301+
to path-style addressing in `auto` mode, because dotted names create
302+
multi-level subdomains that break TLS wildcard certificate validation.
303+
304+
Example using path-style for backwards compatibility:
305+
306+
```
307+
s3://my-bucket/key?region=us-east-1&addressing-style=path
308+
```
309+
310+
Additionally, TCP keep-alive is now enabled on all HTTP connections, preventing
311+
idle connections from being silently dropped by intermediate network devices
312+
(NATs, firewalls, load balancers).
313+

src/libstore-tests/s3-url.cc

Lines changed: 237 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,33 @@ INSTANTIATE_TEST_SUITE_P(
104104
},
105105
},
106106
"with_absolute_endpoint_uri",
107+
},
108+
ParsedS3URLTestCase{
109+
"s3://bucket/key?addressing-style=virtual",
110+
{
111+
.bucket = "bucket",
112+
.key = {"key"},
113+
.addressingStyle = S3AddressingStyle::Virtual,
114+
},
115+
"with_addressing_style_virtual",
116+
},
117+
ParsedS3URLTestCase{
118+
"s3://bucket/key?addressing-style=path",
119+
{
120+
.bucket = "bucket",
121+
.key = {"key"},
122+
.addressingStyle = S3AddressingStyle::Path,
123+
},
124+
"with_addressing_style_path",
125+
},
126+
ParsedS3URLTestCase{
127+
"s3://bucket/key?addressing-style=auto",
128+
{
129+
.bucket = "bucket",
130+
.key = {"key"},
131+
.addressingStyle = S3AddressingStyle::Auto,
132+
},
133+
"with_addressing_style_auto",
107134
}),
108135
[](const ::testing::TestParamInfo<ParsedS3URLTestCase> & info) { return info.param.description; });
109136

@@ -138,6 +165,26 @@ INSTANTIATE_TEST_SUITE_P(
138165
InvalidS3URLTestCase{"s3://bucket", "error: URI has a missing or invalid key", "missing_key"}),
139166
[](const ::testing::TestParamInfo<InvalidS3URLTestCase> & info) { return info.param.description; });
140167

168+
TEST(ParsedS3URLTest, invalidAddressingStyleThrows)
169+
{
170+
ASSERT_THROW(ParsedS3URL::parse(parseURL("s3://bucket/key?addressing-style=bogus")), InvalidS3AddressingStyle);
171+
}
172+
173+
TEST(ParsedS3URLTest, virtualStyleWithAuthoritylessEndpointThrows)
174+
{
175+
ParsedS3URL input{
176+
.bucket = "bucket",
177+
.key = {"key"},
178+
.addressingStyle = S3AddressingStyle::Virtual,
179+
.endpoint =
180+
ParsedURL{
181+
.scheme = "file",
182+
.path = {"", "some", "path"},
183+
},
184+
};
185+
ASSERT_THROW(input.toHttpsUrl(), nix::Error);
186+
}
187+
141188
// =============================================================================
142189
// S3 URL to HTTPS Conversion Tests
143190
// =============================================================================
@@ -166,17 +213,18 @@ INSTANTIATE_TEST_SUITE_P(
166213
S3ToHttpsConversion,
167214
S3ToHttpsConversionTest,
168215
::testing::Values(
216+
// Default (auto) addressing style: virtual-hosted for standard AWS endpoints
169217
S3ToHttpsConversionTestCase{
170218
ParsedS3URL{
171219
.bucket = "my-bucket",
172220
.key = {"my-key.txt"},
173221
},
174222
ParsedURL{
175223
.scheme = "https",
176-
.authority = ParsedURL::Authority{.host = "s3.us-east-1.amazonaws.com"},
177-
.path = {"", "my-bucket", "my-key.txt"},
224+
.authority = ParsedURL::Authority{.host = "my-bucket.s3.us-east-1.amazonaws.com"},
225+
.path = {"", "my-key.txt"},
178226
},
179-
"https://s3.us-east-1.amazonaws.com/my-bucket/my-key.txt",
227+
"https://my-bucket.s3.us-east-1.amazonaws.com/my-key.txt",
180228
"basic_s3_default_region",
181229
},
182230
S3ToHttpsConversionTestCase{
@@ -187,12 +235,13 @@ INSTANTIATE_TEST_SUITE_P(
187235
},
188236
ParsedURL{
189237
.scheme = "https",
190-
.authority = ParsedURL::Authority{.host = "s3.eu-west-1.amazonaws.com"},
191-
.path = {"", "prod-cache", "nix", "store", "abc123.nar.xz"},
238+
.authority = ParsedURL::Authority{.host = "prod-cache.s3.eu-west-1.amazonaws.com"},
239+
.path = {"", "nix", "store", "abc123.nar.xz"},
192240
},
193-
"https://s3.eu-west-1.amazonaws.com/prod-cache/nix/store/abc123.nar.xz",
241+
"https://prod-cache.s3.eu-west-1.amazonaws.com/nix/store/abc123.nar.xz",
194242
"with_eu_west_1_region",
195243
},
244+
// Custom endpoint authority: path-style by default
196245
S3ToHttpsConversionTestCase{
197246
ParsedS3URL{
198247
.bucket = "bucket",
@@ -208,6 +257,7 @@ INSTANTIATE_TEST_SUITE_P(
208257
"http://custom.s3.com/bucket/key",
209258
"custom_endpoint_authority",
210259
},
260+
// Custom endpoint URL: path-style by default
211261
S3ToHttpsConversionTestCase{
212262
ParsedS3URL{
213263
.bucket = "bucket",
@@ -236,10 +286,10 @@ INSTANTIATE_TEST_SUITE_P(
236286
},
237287
ParsedURL{
238288
.scheme = "https",
239-
.authority = ParsedURL::Authority{.host = "s3.ap-southeast-2.amazonaws.com"},
240-
.path = {"", "bucket", "path", "to", "file.txt"},
289+
.authority = ParsedURL::Authority{.host = "bucket.s3.ap-southeast-2.amazonaws.com"},
290+
.path = {"", "path", "to", "file.txt"},
241291
},
242-
"https://s3.ap-southeast-2.amazonaws.com/bucket/path/to/file.txt",
292+
"https://bucket.s3.ap-southeast-2.amazonaws.com/path/to/file.txt",
243293
"complex_path_and_region",
244294
},
245295
S3ToHttpsConversionTestCase{
@@ -250,11 +300,11 @@ INSTANTIATE_TEST_SUITE_P(
250300
},
251301
ParsedURL{
252302
.scheme = "https",
253-
.authority = ParsedURL::Authority{.host = "s3.us-east-1.amazonaws.com"},
254-
.path = {"", "my-bucket", "my-key.txt"},
303+
.authority = ParsedURL::Authority{.host = "my-bucket.s3.us-east-1.amazonaws.com"},
304+
.path = {"", "my-key.txt"},
255305
.query = {{"versionId", "abc123xyz"}},
256306
},
257-
"https://s3.us-east-1.amazonaws.com/my-bucket/my-key.txt?versionId=abc123xyz",
307+
"https://my-bucket.s3.us-east-1.amazonaws.com/my-key.txt?versionId=abc123xyz",
258308
"with_versionId",
259309
},
260310
S3ToHttpsConversionTestCase{
@@ -266,13 +316,185 @@ INSTANTIATE_TEST_SUITE_P(
266316
},
267317
ParsedURL{
268318
.scheme = "https",
269-
.authority = ParsedURL::Authority{.host = "s3.eu-west-1.amazonaws.com"},
270-
.path = {"", "versioned-bucket", "path", "to", "object"},
319+
.authority = ParsedURL::Authority{.host = "versioned-bucket.s3.eu-west-1.amazonaws.com"},
320+
.path = {"", "path", "to", "object"},
271321
.query = {{"versionId", "version456"}},
272322
},
273-
"https://s3.eu-west-1.amazonaws.com/versioned-bucket/path/to/object?versionId=version456",
323+
"https://versioned-bucket.s3.eu-west-1.amazonaws.com/path/to/object?versionId=version456",
274324
"with_region_and_versionId",
325+
},
326+
// Explicit addressing-style=path forces path-style on standard AWS endpoints
327+
S3ToHttpsConversionTestCase{
328+
ParsedS3URL{
329+
.bucket = "my-bucket",
330+
.key = {"my-key.txt"},
331+
.region = "us-west-2",
332+
.addressingStyle = S3AddressingStyle::Path,
333+
},
334+
ParsedURL{
335+
.scheme = "https",
336+
.authority = ParsedURL::Authority{.host = "s3.us-west-2.amazonaws.com"},
337+
.path = {"", "my-bucket", "my-key.txt"},
338+
},
339+
"https://s3.us-west-2.amazonaws.com/my-bucket/my-key.txt",
340+
"explicit_path_style",
341+
},
342+
// Explicit addressing-style=virtual forces virtual-hosted-style on custom endpoints
343+
S3ToHttpsConversionTestCase{
344+
ParsedS3URL{
345+
.bucket = "bucket",
346+
.key = {"key"},
347+
.scheme = "http",
348+
.addressingStyle = S3AddressingStyle::Virtual,
349+
.endpoint = ParsedURL::Authority{.host = "custom.s3.com"},
350+
},
351+
ParsedURL{
352+
.scheme = "http",
353+
.authority = ParsedURL::Authority{.host = "bucket.custom.s3.com"},
354+
.path = {"", "key"},
355+
},
356+
"http://bucket.custom.s3.com/key",
357+
"explicit_virtual_style_custom_endpoint",
358+
},
359+
// Explicit addressing-style=virtual with full endpoint URL
360+
S3ToHttpsConversionTestCase{
361+
ParsedS3URL{
362+
.bucket = "bucket",
363+
.key = {"key"},
364+
.addressingStyle = S3AddressingStyle::Virtual,
365+
.endpoint =
366+
ParsedURL{
367+
.scheme = "http",
368+
.authority = ParsedURL::Authority{.host = "server", .port = 9000},
369+
.path = {""},
370+
},
371+
},
372+
ParsedURL{
373+
.scheme = "http",
374+
.authority = ParsedURL::Authority{.host = "bucket.server", .port = 9000},
375+
.path = {"", "key"},
376+
},
377+
"http://bucket.server:9000/key",
378+
"explicit_virtual_style_full_endpoint_url",
379+
},
380+
// Dotted bucket names work normally with explicit path-style
381+
S3ToHttpsConversionTestCase{
382+
ParsedS3URL{
383+
.bucket = "my.bucket",
384+
.key = {"key"},
385+
.addressingStyle = S3AddressingStyle::Path,
386+
},
387+
ParsedURL{
388+
.scheme = "https",
389+
.authority = ParsedURL::Authority{.host = "s3.us-east-1.amazonaws.com"},
390+
.path = {"", "my.bucket", "key"},
391+
},
392+
"https://s3.us-east-1.amazonaws.com/my.bucket/key",
393+
"dotted_bucket_with_path_style",
394+
},
395+
// Dotted bucket names fall back to path-style with auto on standard AWS endpoints
396+
S3ToHttpsConversionTestCase{
397+
ParsedS3URL{
398+
.bucket = "my.bucket.name",
399+
.key = {"key"},
400+
},
401+
ParsedURL{
402+
.scheme = "https",
403+
.authority = ParsedURL::Authority{.host = "s3.us-east-1.amazonaws.com"},
404+
.path = {"", "my.bucket.name", "key"},
405+
},
406+
"https://s3.us-east-1.amazonaws.com/my.bucket.name/key",
407+
"dotted_bucket_with_auto_style_on_aws",
408+
},
409+
// Dotted bucket names work with auto style on custom endpoints (auto = path-style)
410+
S3ToHttpsConversionTestCase{
411+
ParsedS3URL{
412+
.bucket = "my.bucket",
413+
.key = {"key"},
414+
.endpoint = ParsedURL::Authority{.host = "minio.local"},
415+
},
416+
ParsedURL{
417+
.scheme = "https",
418+
.authority = ParsedURL::Authority{.host = "minio.local"},
419+
.path = {"", "my.bucket", "key"},
420+
},
421+
"https://minio.local/my.bucket/key",
422+
"dotted_bucket_with_auto_style_custom_endpoint",
275423
}),
276424
[](const ::testing::TestParamInfo<S3ToHttpsConversionTestCase> & info) { return info.param.description; });
277425

426+
// =============================================================================
427+
// S3 URL to HTTPS Conversion Error Tests
428+
// =============================================================================
429+
430+
struct S3ToHttpsConversionErrorTestCase
431+
{
432+
ParsedS3URL input;
433+
std::string description;
434+
};
435+
436+
class S3ToHttpsConversionErrorTest : public ::testing::WithParamInterface<S3ToHttpsConversionErrorTestCase>,
437+
public ::testing::Test
438+
{};
439+
440+
TEST_P(S3ToHttpsConversionErrorTest, ThrowsOnConversion)
441+
{
442+
auto & [input, description] = GetParam();
443+
ASSERT_THROW(input.toHttpsUrl(), nix::Error);
444+
}
445+
446+
INSTANTIATE_TEST_SUITE_P(
447+
S3ToHttpsConversionErrors,
448+
S3ToHttpsConversionErrorTest,
449+
::testing::Values(
450+
S3ToHttpsConversionErrorTestCase{
451+
ParsedS3URL{
452+
.bucket = "bucket",
453+
.key = {"key"},
454+
.addressingStyle = S3AddressingStyle::Virtual,
455+
.endpoint = ParsedURL::Authority{.host = ""},
456+
},
457+
"virtual_style_with_empty_host_authority",
458+
},
459+
S3ToHttpsConversionErrorTestCase{
460+
ParsedS3URL{
461+
.bucket = "my.bucket",
462+
.key = {"key"},
463+
.addressingStyle = S3AddressingStyle::Virtual,
464+
},
465+
"dotted_bucket_with_explicit_virtual_style",
466+
},
467+
S3ToHttpsConversionErrorTestCase{
468+
ParsedS3URL{
469+
.bucket = "my.bucket.name",
470+
.key = {"key"},
471+
.addressingStyle = S3AddressingStyle::Virtual,
472+
},
473+
"dotted_bucket_with_explicit_virtual_style_multi_dot",
474+
},
475+
S3ToHttpsConversionErrorTestCase{
476+
ParsedS3URL{
477+
.bucket = "my.bucket",
478+
.key = {"key"},
479+
.addressingStyle = S3AddressingStyle::Virtual,
480+
.endpoint = ParsedURL::Authority{.host = "minio.local"},
481+
},
482+
"dotted_bucket_with_explicit_virtual_style_custom_authority",
483+
},
484+
S3ToHttpsConversionErrorTestCase{
485+
ParsedS3URL{
486+
.bucket = "my.bucket",
487+
.key = {"key"},
488+
.addressingStyle = S3AddressingStyle::Virtual,
489+
.endpoint =
490+
ParsedURL{
491+
.scheme = "http",
492+
.authority = ParsedURL::Authority{.host = "minio.local", .port = 9000},
493+
.path = {""},
494+
},
495+
},
496+
"dotted_bucket_with_explicit_virtual_style_full_endpoint_url",
497+
}),
498+
[](const ::testing::TestParamInfo<S3ToHttpsConversionErrorTestCase> & info) { return info.param.description; });
499+
278500
} // namespace nix

src/libstore/daemon.cc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,10 @@ static void performOp(
858858
auto path = WorkerProto::Serialise<StorePath>::read(*store, rconn);
859859
std::shared_ptr<const ValidPathInfo> info;
860860
logger->startWork();
861-
info = store->queryPathInfo(path);
861+
try {
862+
info = store->queryPathInfo(path);
863+
} catch (InvalidPath &) {
864+
}
862865
logger->stopWork();
863866
if (info) {
864867
conn.to << 1;

src/libstore/filetransfer.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,12 @@ struct curlFileTransfer : public FileTransfer
502502

503503
curl_easy_setopt(req, CURLOPT_CONNECTTIMEOUT, fileTransferSettings.connectTimeout.get());
504504

505+
// Enable TCP keep-alive so that idle connections in curl's reuse pool
506+
// are not silently dropped by NATs, firewalls, or load balancers.
507+
curl_easy_setopt(req, CURLOPT_TCP_KEEPALIVE, 1L);
508+
curl_easy_setopt(req, CURLOPT_TCP_KEEPIDLE, 60L);
509+
curl_easy_setopt(req, CURLOPT_TCP_KEEPINTVL, 60L);
510+
505511
curl_easy_setopt(req, CURLOPT_LOW_SPEED_LIMIT, 1L);
506512
curl_easy_setopt(req, CURLOPT_LOW_SPEED_TIME, fileTransferSettings.stalledDownloadTimeout.get());
507513

0 commit comments

Comments
 (0)