From 14a4dd6dbe8293decb0b51e27fb6dd4f2c894c83 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Mon, 3 Nov 2025 23:07:40 -0800 Subject: [PATCH 1/4] Conf: borrow identifiers as &str from configuration pool. This allows us to skip revalidation of UTF-8 identifier strings and reallocation of the order object. See also 511be2a. --- src/conf.rs | 18 +++++++--- src/conf/identifier.rs | 15 -------- src/conf/issuer.rs | 6 ++-- src/conf/order.rs | 81 ++++++++++++++++++------------------------ src/lib.rs | 9 +---- 5 files changed, 52 insertions(+), 77 deletions(-) diff --git a/src/conf.rs b/src/conf.rs index 05f41c3..cbaa64d 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -53,7 +53,7 @@ pub struct AcmeServerConfig { pub issuer: ngx_str_t, // Only one certificate order per server block is currently allowed. For multiple entries we // will have to implement certificate selection in the variable handler. - pub order: Option>, + pub order: Option>, } pub static mut NGX_HTTP_ACME_COMMANDS: [ngx_command_t; 4] = [ @@ -308,7 +308,7 @@ extern "C" fn cmd_add_certificate( return c"\"issuer\" is missing".as_ptr().cast_mut(); } - let mut order = CertificateOrder::::new_in(cf.pool()); + let mut order = CertificateOrder::<&'static str, Pool>::new_in(cf.pool()); for value in &args[2..] { if let Some(key) = value.strip_prefix(b"key=") { @@ -320,7 +320,17 @@ extern "C" fn cmd_add_certificate( continue; } - if let Err(err) = order.try_add_identifier(value) { + if value.is_empty() { + return NGX_CONF_INVALID_VALUE; + } + + // SAFETY: the value is not empty, well aligned, and the conversion result is assigned to an + // object in the same pool. + let Ok(value) = (unsafe { conf_value_to_str(value) }) else { + return NGX_CONF_INVALID_VALUE; + }; + + if let Err(err) = order.try_add_identifier(cf, value) { return cf.error(args[0], &err); } } @@ -779,7 +789,7 @@ fn conf_check_nargs(cmd: &ngx_command_t, nargs: ngx_uint_t) -> bool { /// all the configuration objects. But this process role is not capable of serving connections or /// running background tasks, and thus will not create additional borrows with potentially extended /// lifetime. -unsafe fn conf_value_to_str(value: &ngx_str_t) -> Result<&'static str, core::str::Utf8Error> { +pub unsafe fn conf_value_to_str(value: &ngx_str_t) -> Result<&'static str, core::str::Utf8Error> { if value.len == 0 { Ok("") } else { diff --git a/src/conf/identifier.rs b/src/conf/identifier.rs index 65e5eb0..ff0bb8a 100644 --- a/src/conf/identifier.rs +++ b/src/conf/identifier.rs @@ -39,21 +39,6 @@ impl Identifier { } } - /// Borrows the current identifier with a str reference as an underlying data. - pub fn as_str(&self) -> Result, str::Utf8Error> - where - S: AsRef<[u8]>, - { - match self { - Identifier::Dns(value) => str::from_utf8(value.as_ref()).map(Identifier::Dns), - Identifier::Ip(value) => str::from_utf8(value.as_ref()).map(Identifier::Ip), - Identifier::Other { kind, value } => Ok(Identifier::Other { - kind: str::from_utf8(kind.as_ref())?, - value: str::from_utf8(value.as_ref())?, - }), - } - } - pub fn value(&self) -> &S { match self { Identifier::Dns(value) => value, diff --git a/src/conf/issuer.rs b/src/conf/issuer.rs index 8d91d9d..5d69444 100644 --- a/src/conf/issuer.rs +++ b/src/conf/issuer.rs @@ -60,7 +60,7 @@ pub struct Issuer { // Generated fields // ngx_ssl_t stores a pointer to itself in SSL_CTX ex_data. pub ssl: Box, - pub orders: RbTreeMap, CertificateContext, Pool>, + pub orders: RbTreeMap, CertificateContext, Pool>, pub pkey: Option>, pub data: Option<&'static RwLock>, } @@ -228,7 +228,7 @@ impl Issuer { pub fn add_certificate_order( &mut self, cf: &mut ngx_conf_t, - order: &CertificateOrder, + order: &CertificateOrder<&'static str, Pool>, ) -> Result<(), Status> { if self.orders.get(order).is_none() { ngx_log_debug!( @@ -412,7 +412,7 @@ impl StateDir { pub fn load_certificate( &self, cf: &mut ngx_conf_t, - order: &CertificateOrder, + order: &CertificateOrder<&'static str, Pool>, ) -> Result, CachedCertificateError> { use openssl_foreign_types::ForeignType; #[cfg(ngx_ssl_cache)] diff --git a/src/conf/order.rs b/src/conf/order.rs index 2e5f23c..34bef52 100644 --- a/src/conf/order.rs +++ b/src/conf/order.rs @@ -3,7 +3,7 @@ // This source code is licensed under the Apache License, Version 2.0 license found in the // LICENSE file in the root directory of this source tree. -use core::fmt::{self, Write}; +use core::fmt; use core::hash::{self, Hash, Hasher}; use core::net::IpAddr; use core::str::Utf8Error; @@ -60,20 +60,6 @@ where dns.or_else(|| self.identifiers.first()) .map(Identifier::value) } - - pub fn to_str_order(&self, alloc: NewA) -> CertificateOrder<&str, NewA> - where - NewA: Allocator + Clone, - S: AsRef<[u8]>, - { - let mut identifiers = Vec::, NewA>::new_in(alloc); - identifiers.extend(self.identifiers.iter().map(|x| x.as_str().unwrap())); - - CertificateOrder { - identifiers, - key: self.key.clone(), - } - } } impl Hash for CertificateOrder @@ -150,8 +136,6 @@ where pub enum IdentifierError { #[error("memory allocation failed")] Alloc(#[from] AllocError), - #[error("empty server name")] - Empty, #[error("invalid server name")] Invalid, #[error("invalid UTF-8 string")] @@ -160,9 +144,9 @@ pub enum IdentifierError { Wildcard, } -impl CertificateOrder { +impl CertificateOrder<&'static str, Pool> { #[inline] - fn push(&mut self, id: Identifier) -> Result<(), AllocError> { + fn push(&mut self, id: Identifier<&'static str>) -> Result<(), AllocError> { self.identifiers.try_reserve(1).map_err(|_| AllocError)?; self.identifiers.push(id); Ok(()) @@ -190,29 +174,30 @@ impl CertificateOrder { continue; } - self.try_add_identifier(&server_name.name)?; + // SAFETY: the value is not empty, well aligned, and the conversion result is assigned + // to an object in the same pool. + let value = unsafe { super::conf_value_to_str(&server_name.name)? }; + + self.try_add_identifier(cf, value)?; } Ok(()) } - pub fn try_add_identifier(&mut self, value: &ngx_str_t) -> Result<(), IdentifierError> { - if value.is_empty() { - return Err(IdentifierError::Empty); - } - - if core::str::from_utf8(value.as_ref())? - .parse::() - .is_ok() - { - return self.push(Identifier::Ip(*value)).map_err(Into::into); + pub fn try_add_identifier( + &mut self, + cf: &ngx_conf_t, + value: &'static str, + ) -> Result<(), IdentifierError> { + if value.parse::().is_ok() { + return self.push(Identifier::Ip(value)).map_err(Into::into); } - if value.as_bytes().contains(&b'*') { + if value.contains('*') { return Err(IdentifierError::Wildcard); } - let host = validate_host(self.identifiers.allocator(), *value).map_err(|st| { + let host = validate_host(cf, value).map_err(|st| { if st == Status::NGX_ERROR { IdentifierError::Alloc(AllocError) } else { @@ -226,18 +211,14 @@ impl CertificateOrder { * See */ - if let Some(host) = host.strip_prefix(b".") { - let mut www = NgxString::new_in(self.identifiers.allocator()); - www.try_reserve_exact(host.len + 4) + if let Some(host) = host.strip_prefix(".") { + let mut www = Vec::new_in(self.identifiers.allocator().clone()); + www.try_reserve_exact(host.len() + 4) .map_err(|_| AllocError)?; - // write to a buffer of sufficient size will succeed - let _ = write!(&mut www, "www.{host}"); - - let parts = www.into_raw_parts(); - let www = ngx_str_t { - data: parts.0, - len: parts.1, - }; + www.extend_from_slice(b"www."); + www.extend_from_slice(host.as_bytes()); + // The buffer is owned by ngx_pool_t and does not leak. + let www = core::str::from_utf8(www.leak())?; self.push(Identifier::Dns(www))?; self.push(Identifier::Dns(host))?; @@ -273,11 +254,17 @@ where } } -fn validate_host(pool: &Pool, mut host: ngx_str_t) -> Result { - let mut pool = pool.clone(); - let rc = Status(unsafe { nginx_sys::ngx_http_validate_host(&mut host, pool.as_mut(), 1) }); +/// Checks if the value is a valid domain name and returns a canonical (lowercase) form, +/// reallocated on the configuration pool if necessary. +fn validate_host(cf: &ngx_conf_t, host: &'static str) -> Result<&'static str, Status> { + let mut host = ngx_str_t { + data: host.as_ptr().cast_mut(), + len: host.len(), + }; + let rc = Status(unsafe { nginx_sys::ngx_http_validate_host(&mut host, cf.pool, 0) }); if rc != Status::NGX_OK { return Err(rc); } - Ok(host) + + unsafe { super::conf_value_to_str(&host) }.map_err(|_| Status::NGX_ERROR) } diff --git a/src/lib.rs b/src/lib.rs index 9ab46ae..ace6779 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,6 @@ use nginx_sys::{ ngx_conf_t, ngx_cycle_t, ngx_http_add_variable, ngx_http_module_t, ngx_int_t, ngx_module_t, ngx_uint_t, NGX_HTTP_MODULE, NGX_LOG_ERR, NGX_LOG_INFO, NGX_LOG_NOTICE, NGX_LOG_WARN, }; -use ngx::allocator::AllocError; use ngx::core::{Status, NGX_CONF_ERROR, NGX_CONF_OK}; use ngx::http::{HttpModule, HttpModuleMainConf, HttpModuleServerConf, Merge}; use ngx::log::ngx_cycle_log; @@ -334,13 +333,7 @@ async fn ngx_http_acme_update_certificates_for_issuer( } } - let alloc = crate::util::OwnedPool::new(nginx_sys::NGX_DEFAULT_POOL_SIZE as _, log) - .map_err(|_| AllocError)?; - - // Acme client wants &str and we already validated that the identifiers are valid UTF-8. - let str_order = order.to_str_order(&*alloc); - - let cert_next = match client.new_certificate(&str_order).await { + let cert_next = match client.new_certificate(order).await { Ok(ref val) => { let pkey = Zeroizing::new(val.pkey.private_key_to_pem_pkcs8()?); let x509 = X509::from_pem(&val.chain)?; From 18671e0b7683e7614f5e59ec25bd74b08d331f02 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Tue, 4 Nov 2025 10:58:40 -0800 Subject: [PATCH 2/4] Conf: use canonical textual form for IP identifiers. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As stated in RFC8738 ยง 3: : The value field of the identifier MUST contain the textual form of the : address as defined in Section 2.1 of [RFC1123] for IPv4 and in Section : 4 of [RFC5952] for IPv6. This is the last piece required before declaring RFC8738 support. Fixes #5. --- src/conf/order.rs | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/conf/order.rs b/src/conf/order.rs index 34bef52..4b186e5 100644 --- a/src/conf/order.rs +++ b/src/conf/order.rs @@ -5,7 +5,6 @@ use core::fmt; use core::hash::{self, Hash, Hasher}; -use core::net::IpAddr; use core::str::Utf8Error; use nginx_sys::{ngx_conf_t, ngx_http_server_name_t, ngx_str_t}; @@ -16,6 +15,7 @@ use ngx::ngx_log_error; use siphasher::sip::SipHasher; use thiserror::Error; +use crate::conf::ext::NgxConfExt; use crate::conf::identifier::Identifier; use crate::conf::pkey::PrivateKey; @@ -189,8 +189,8 @@ impl CertificateOrder<&'static str, Pool> { cf: &ngx_conf_t, value: &'static str, ) -> Result<(), IdentifierError> { - if value.parse::().is_ok() { - return self.push(Identifier::Ip(value)).map_err(Into::into); + if let Some(addr) = parse_ip_identifier(cf, value)? { + return self.push(Identifier::Ip(addr)).map_err(Into::into); } if value.contains('*') { @@ -254,6 +254,41 @@ where } } +/// Attempts to parse the value as an IP address, returning `Some(...)` on success. +/// +/// The address will be converted to a canonical textual form and reallocated on the +/// configuration pool if necessary. +fn parse_ip_identifier( + cf: &ngx_conf_t, + value: &'static str, +) -> Result, AllocError> { + const INET6_ADDRSTRLEN: usize = "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255".len(); + + let Ok(addr) = value.parse::() else { + return Ok(None); + }; + + let mut buf = [0u8; INET6_ADDRSTRLEN]; + let mut cur = std::io::Cursor::new(&mut buf[..]); + // Formatting IP address to a sufficiently large buffer should always succeed + let _ = std::io::Write::write_fmt(&mut cur, format_args!("{addr}")); + let len = cur.position() as usize; + let buf = &buf[..len]; + + if buf == value.as_bytes() { + return Ok(Some(value)); + } + + let mut out = Vec::new_in(cf.pool()); + out.try_reserve_exact(buf.len()).map_err(|_| AllocError)?; + out.extend_from_slice(buf); + // SAFETY: formatted IpAddr is always a valid ASCII string. + // The buffer is owned by the ngx_pool_t and does not leak. + let out = unsafe { core::str::from_utf8_unchecked(out.leak()) }; + + Ok(Some(out)) +} + /// Checks if the value is a valid domain name and returns a canonical (lowercase) form, /// reallocated on the configuration pool if necessary. fn validate_host(cf: &ngx_conf_t, host: &'static str) -> Result<&'static str, Status> { From fe480295d6d3100ed890c1f2ab7653e50fc3d626 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Wed, 5 Nov 2025 12:11:39 -0800 Subject: [PATCH 3/4] Docs: add support status and example for IP identifiers. --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 15f8dee..cda8c41 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,13 @@ The module implements following specifications: - Only HTTP-01 challenge type is supported - [RFC8737] (ACME TLS Application-Layer Protocol Negotiation (ALPN) Challenge Extension) +- [RFC8738] (ACME IP Identifier Validation Extension) - [draft-ietf-acme-profiles] (ACME Profiles Extension, version 00) [NGINX]: https://nginx.org/ [RFC8555]: https://datatracker.ietf.org/doc/html/rfc8555 [RFC8737]: https://datatracker.ietf.org/doc/html/rfc8737 +[RFC8738]: https://datatracker.ietf.org/doc/html/rfc8738 [draft-ietf-acme-profiles]: https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/ ## Getting Started @@ -162,7 +164,10 @@ acme_shared_zone zone=ngx_acme_shared:1M; server { listen 443 ssl; - server_name .example.test; + server_name .example.test + 192.0.2.1 # not supported by some ACME servers + 2001:db8::1 # not supported by some ACME servers + ; acme_certificate example; From 628fa6286740d93e7f03899978e5324a45bc6b49 Mon Sep 17 00:00:00 2001 From: Aleksei Bavshin Date: Wed, 5 Nov 2025 11:52:26 -0800 Subject: [PATCH 4/4] Tests: IP identifier support tests. --- .github/workflows/ci.yaml | 1 + .github/workflows/sanitizers.yaml | 1 + t/acme_ipv4_identifier.t | 161 +++++++++++++++++++++++++ t/acme_ipv6_identifier.t | 190 ++++++++++++++++++++++++++++++ 4 files changed, 353 insertions(+) create mode 100644 t/acme_ipv4_identifier.t create mode 100644 t/acme_ipv6_identifier.t diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 46afe48..387627e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -127,6 +127,7 @@ jobs: - uses: perl-actions/install-with-cpm@8b1a9840b26cc3885ae2889749a48629be2501b0 # v1.9 with: install: | + IO::Socket::INET6 IO::Socket::SSL TimeDate diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index b9be5b9..b410ebe 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -18,6 +18,7 @@ env: make openssl patch which perl-Digest-SHA perl-FindBin + perl-IO-Socket-INET6 perl-IO-Socket-SSL perl-Test-Harness perl-Test-Simple diff --git a/t/acme_ipv4_identifier.t b/t/acme_ipv4_identifier.t new file mode 100644 index 0000000..3e95f40 --- /dev/null +++ b/t/acme_ipv4_identifier.t @@ -0,0 +1,161 @@ +#!/usr/bin/perl + +# Copyright (c) F5, Inc. +# +# This source code is licensed under the Apache License, Version 2.0 license +# found in the LICENSE file in the root directory of this source tree. + +# Tests for ACME client: IPv4 identifier support. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::ACME; +use Test::Nginx::DNS; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_ssl socket_ssl/) + ->has_daemon('openssl'); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + resolver 127.0.0.1:%%PORT_8980_UDP%%; + + acme_issuer default { + uri https://acme.test:%%PORT_9000%%/dir; + challenge http-01; + ssl_trusted_certificate acme.test.crt; + state_path %%TESTDIR%%/acme_default; + accept_terms_of_service; + } + + acme_issuer tls-alpn { + uri https://acme.test:%%PORT_9000%%/dir; + challenge tls-alpn-01; + ssl_trusted_certificate acme.test.crt; + state_path %%TESTDIR%%/acme_tls-alpn; + accept_terms_of_service; + } + + server { + listen 127.0.0.1:8080; + } + + server { + listen 127.0.0.1:8443 ssl; + server_name 127.0.0.1; + + acme_certificate default; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + } + + server { + listen 127.0.0.1:8444 ssl; + server_name 127.0.0.1; + + acme_certificate tls-alpn; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('acme.test') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +my $dp = port(8980, udp=>1); +my @dc = ( + { name => 'acme.test', A => '127.0.0.1' }, +); + +my $acme = Test::Nginx::ACME->new($t, port(9000), port(9001), + $t->testdir . '/acme.test.crt', + $t->testdir . '/acme.test.key', + http_port => port(8080), + tls_port => port(8444), + dns_port => $dp, + nosleep => 1, +); + +$t->run_daemon(\&Test::Nginx::DNS::dns_test_daemon, $t, $dp, \@dc); +$t->waitforfile($t->testdir . '/' . $dp); + +$t->run_daemon(\&Test::Nginx::ACME::acme_test_daemon, $t, $acme); +$t->waitforsocket('127.0.0.1:' . $acme->port()); +$t->write_file('acme-root.crt', $acme->trusted_ca()); + +$t->write_file('index.html', 'SUCCESS'); +$t->plan(2)->run(); + +############################################################################### + +$acme->wait_certificate('acme_default/127.0.0.1') or die "no certificate"; +$acme->wait_certificate('acme_tls-alpn/127.0.0.1') or die "no certificate"; + +like(get('127.0.0.1', 8443, 'acme-root'), qr/SUCCESS/, + 'ipv4 cert via http-01'); +like(get('127.0.0.1', 8444, 'acme-root'), qr/SUCCESS/, + 'ipv4 cert via tls-alpn-01'); + +############################################################################### + +sub get { + my ($addr, $port, $ca) = @_; + + $ca = undef if $IO::Socket::SSL::VERSION < 2.062 + || !eval { Net::SSLeay::X509_V_FLAG_PARTIAL_CHAIN() }; + + http_get('/', + PeerAddr => $addr, + PeerPort => port($port), + SSL => 1, + $ca ? ( + SSL_ca_file => "$d/$ca.crt", + SSL_verifycn_name => $addr, + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(), + ) : () + ); +} + +############################################################################### diff --git a/t/acme_ipv6_identifier.t b/t/acme_ipv6_identifier.t new file mode 100644 index 0000000..bfa21db --- /dev/null +++ b/t/acme_ipv6_identifier.t @@ -0,0 +1,190 @@ +#!/usr/bin/perl + +# Copyright (c) F5, Inc. +# +# This source code is licensed under the Apache License, Version 2.0 license +# found in the LICENSE file in the root directory of this source tree. + +# Tests for ACME client: IPv6 identifier support. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::ACME; +use Test::Nginx::DNS; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_ssl socket_ssl/) + ->has_daemon('openssl'); + +eval { require IO::Socket::INET6; }; +plan(skip_all => 'IO::Socket::INET6 is not installed') if $@; + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + resolver 127.0.0.1:%%PORT_8980_UDP%%; + + acme_issuer default { + uri https://acme.test:%%PORT_9000%%/dir; + challenge http-01; + ssl_trusted_certificate acme.test.crt; + state_path %%TESTDIR%%/acme_default; + accept_terms_of_service; + } + + acme_issuer tls-alpn { + uri https://acme.test:%%PORT_9000%%/dir; + challenge tls-alpn-01; + ssl_trusted_certificate acme.test.crt; + state_path %%TESTDIR%%/acme_tls-alpn; + accept_terms_of_service; + } + + server { + listen [::1]:%%PORT_8080%%; + } + + server { + listen [::1]:%%PORT_8443%% ssl; + server_name 0000:0000:0000:0000:0000:0000:0:1; + + acme_certificate default; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + } + + server { + listen [::1]:%%PORT_8444%% ssl; + server_name ::1; + + acme_certificate tls-alpn; + + ssl_certificate $acme_certificate; + ssl_certificate_key $acme_certificate_key; + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('acme.test') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +my $dp = port(8980, udp=>1); +my @dc = ( + { name => 'acme.test', A => '127.0.0.1' }, +); + +my $acme = Test::Nginx::ACME->new($t, port(9000), port(9001), + $t->testdir . '/acme.test.crt', + $t->testdir . '/acme.test.key', + http_port => port(8080), + tls_port => port(8444), + dns_port => $dp, + nosleep => 1, +); + +$t->run_daemon(\&Test::Nginx::DNS::dns_test_daemon, $t, $dp, \@dc); +$t->waitforfile($t->testdir . '/' . $dp); + +$t->run_daemon(\&Test::Nginx::ACME::acme_test_daemon, $t, $acme); +$t->waitforsocket('127.0.0.1:' . $acme->port()); +$t->write_file('acme-root.crt', $acme->trusted_ca()); + +$t->write_file('index.html', 'SUCCESS'); +$t->try_run('no inet6 support')->plan(2); + +############################################################################### + +$acme->wait_certificate('acme_default/::1') or die "no certificate"; +$acme->wait_certificate('acme_tls-alpn/::1') or die "no certificate"; + +like(get('::1', 8443, 'acme-root'), qr/SUCCESS/, 'ipv6 cert via http-01'); +like(get('::1', 8444, 'acme-root'), qr/SUCCESS/, 'ipv6 cert via tls-alpn-01'); + +############################################################################### + +sub get { + my ($addr, $port, $ca) = @_; + + my $s = getconn(@_) || return; + + $ca = undef if $IO::Socket::SSL::VERSION < 2.062 + || !eval { Net::SSLeay::X509_V_FLAG_PARTIAL_CHAIN() }; + + http_get('/', + socket => $s, + SSL => 1, + $ca ? ( + SSL_ca_file => "$d/$ca.crt", + SSL_verifycn_name => $addr, + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(), + ) : () + ); +} + +sub getconn() { + my ($addr, $port) = @_; + my $s; + + eval { + local $SIG{ALRM} = sub { die "timeout\n" }; + local $SIG{PIPE} = sub { die "sigpipe\n" }; + alarm(8); + + $s = IO::Socket::INET6->new( + PeerAddr => $addr, + PeerPort => port($port), + ) + or die "Can't connect to nginx: $!\n"; + + alarm(0); + }; + alarm(0); + + if ($@) { + log_in("died: $@"); + return undef; + } + + return $s; +} + +###############################################################################