ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE / ERR_TLS_CERT_ALTNAME_INVALID with dns interceptor and requestTls
Bug Description
When using the requestTls option (specifically rejectUnauthorized) in conjunction with the DNS interceptor (interceptors.dns()), multiple SSL errors are thrown. The behavior changes depending on the configuration:
- If I remove
.compose([interceptors.dns()]), the issue disappears, and everything works fine. - If I remove
requestTlscompletely, it also works fine even with.compose([interceptors.dns()]). - If I keep
requestTlsbut remove onlyrejectUnauthorized: true(just{ requestTls: {} }), the error persists when.compose([interceptors.dns()])is used.
This suggests an interaction between requestTls and the DNS interceptor that causes SSL issues.
Reproducible By
Example 1: (issue)
import { ProxyAgent, Agent, fetch, interceptors } from 'undici'
async function main () {
await fetch('https://api.ipify.org', {
dispatcher: new Agent()
.compose([interceptors.dns()])
}).then(async x => console.log('My IP:', await x.text()))
await fetch('https://api.ipify.org', {
dispatcher: new ProxyAgent({
uri: 'http://localhost:18080'
})
.compose([interceptors.dns()])
}).then(async x => console.log('Proxy IP:', await x.text()))
const response = await fetch('https://api.ipify.org', {
dispatcher: new ProxyAgent({
uri: 'http://localhost:18080',
requestTls: {
rejectUnauthorized: true
}
})
.compose([interceptors.dns()])
}).then(x => x.text())
console.log('Ip with rejectUnauthorized: true', response)
}
main ()
Result:
My IP: [REDACTED].212.[REDACTED].76
Proxy IP: [REDACTED].111.[REDACTED].50
/home/[REDACTED]/undici-altname-ssl-issue/node_modules/undici/index.js:120
Error.captureStackTrace(err)
^
TypeError: fetch failed
at fetch (/home/[REDACTED]/undici-altname-ssl-issue/node_modules/undici/index.js:120:13)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async main (file:///home/[REDACTED]/undici-altname-ssl-issue/index.js:16:20) {
[cause]: [Error: 40C8661E52720000:error:0A000410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure:../deps/openssl/openssl/ssl/record/rec_layer_s3.c:1590:SSL alert number 40
] {
library: 'SSL routines',
reason: 'sslv3 alert handshake failure',
code: 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'
}
}
Example two: (issue)
import { ProxyAgent, Agent, fetch, interceptors } from 'undici'
async function main () {
await fetch('https://api.ipify.org', {
dispatcher: new Agent()
.compose([interceptors.dns()])
}).then(async x => console.log('My IP:', await x.text()))
await fetch('https://api.ipify.org', {
dispatcher: new ProxyAgent({
uri: 'http://localhost:18080'
})
.compose([interceptors.dns()])
}).then(async x => console.log('Proxy IP:', await x.text()))
const response = await fetch('https://wikipedia.org', {
dispatcher: new ProxyAgent({
uri: 'http://localhost:18080',
requestTls: {
rejectUnauthorized: true
}
})
.compose([interceptors.dns()])
}).then(x => x.text())
console.log(response)
}
main ()
Result:
Click me to show large log
My IP: [REDACTED].212.[REDACTED].76
Proxy IP: [REDACTED].111.[REDACTED].50
/home/[REDACTED]/undici-altname-ssl-issue/node_modules/undici/index.js:120
Error.captureStackTrace(err)
^
TypeError: fetch failed
at fetch (/home/[REDACTED]/undici-altname-ssl-issue/node_modules/undici/index.js:120:13)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async main (file:///home/[REDACTED]/undici-altname-ssl-issue/index.js:16:20) {
[cause]: Error [ERR_TLS_CERT_ALTNAME_INVALID]: Hostname/IP does not match certificate's altnames: IP: 185.15.59.224 is not in the cert's list:
at Object.checkServerIdentity (node:tls:337:12)
at TLSSocket.onConnectSecure (node:_tls_wrap:1684:27)
at TLSSocket.emit (node:events:518:28)
at TLSSocket._finishInit (node:_tls_wrap:1085:8)
at ssl.onhandshakedone (node:_tls_wrap:871:12) {
code: 'ERR_TLS_CERT_ALTNAME_INVALID',
reason: "IP: 185.15.59.224 is not in the cert's list: ",
host: '185.15.59.224',
cert: {
subject: [Object: null prototype] {
C: 'US',
ST: 'California',
L: 'San Francisco',
O: 'Wikimedia Foundation, Inc.',
CN: '*.wikipedia.org'
},
issuer: [Object: null prototype] {
C: 'US',
O: 'DigiCert Inc',
CN: 'DigiCert TLS Hybrid ECC SHA384 2020 CA1'
},
subjectaltname: 'DNS:*.wikipedia.org, DNS:wikimedia.org, DNS:mediawiki.org, DNS:wikibooks.org, DNS:wikidata.org, DNS:wikinews.org, DNS:wikiquote.org, DNS:wikisource.org, DNS:wikiversity.org, DNS:wikivoyage.org, DNS:wiktionary.org, DNS:wikimediafoundation.org, DNS:w.wiki, DNS:wmfusercontent.org, DNS:*.m.wikipedia.org, DNS:*.wikimedia.org, DNS:*.m.wikimedia.org, DNS:*.planet.wikimedia.org, DNS:*.mediawiki.org, DNS:*.m.mediawiki.org, DNS:*.wikibooks.org, DNS:*.m.wikibooks.org, DNS:*.wikidata.org, DNS:*.m.wikidata.org, DNS:*.wikinews.org, DNS:*.m.wikinews.org, DNS:*.wikiquote.org, DNS:*.m.wikiquote.org, DNS:*.wikisource.org, DNS:*.m.wikisource.org, DNS:*.wikiversity.org, DNS:*.m.wikiversity.org, DNS:*.wikivoyage.org, DNS:*.m.wikivoyage.org, DNS:*.wiktionary.org, DNS:*.m.wiktionary.org, DNS:*.wikimediafoundation.org, DNS:*.wmfusercontent.org, DNS:wikipedia.org, DNS:wikifunctions.org, DNS:*.wikifunctions.org',
infoAccess: [Object: null prototype] {
'OCSP - URI': [ 'http://ocsp.digicert.com' ],
'CA Issuers - URI': [
'http://cacerts.digicert.com/DigiCertTLSHybridECCSHA3842020CA1-1.crt'
]
},
ca: false,
bits: 256,
pubkey: Buffer(65) [Uint8Array] [
4, 41, 254, 247, 2, 121, 201, 130, 181, 38, 68,
233, 201, 191, 6, 62, 207, 73, 162, 210, 234, 254,
49, 84, 227, 83, 221, 123, 239, 33, 121, 35, 168,
32, 215, 30, 57, 116, 191, 92, 15, 133, 107, 161,
108, 81, 133, 72, 194, 184, 17, 16, 168, 195, 45,
229, 34, 8, 190, 171, 64, 207, 60, 68, 14
],
asn1Curve: 'prime256v1',
nistCurve: 'P-256',
valid_from: 'Sep 26 00:00:00 2024 GMT',
valid_to: 'Oct 17 23:59:59 2025 GMT',
fingerprint: '0B:3A:AB:D4:5E:55:A4:08:2B:F7:C1:DA:63:37:75:F1:EB:04:6E:A5',
fingerprint256: '40:62:FB:AE:31:5E:7D:29:B8:24:32:78:9D:DC:4B:99:1D:AB:8B:54:ED:DF:76:C8:12:98:9E:22:F1:BA:FD:59',
fingerprint512: 'CB:B4:E6:61:67:8C:FC:EB:DE:DD:11:1D:4E:B3:A2:4D:2C:49:3D:16:66:5F:36:F4:17:7A:AA:CA:9B:D7:6B:F3:09:48:7F:1B:1F:67:22:02:2D:4B:7D:A4:FC:A5:E5:4F:1F:60:E3:AB:48:2B:42:CB:E7:45:16:89:E9:A5:DE:64',
ext_key_usage: [ '1.3.6.1.5.5.7.3.1', '1.3.6.1.5.5.7.3.2' ],
serialNumber: '0C745DCAE53F59103BEDA2477CCCE73A',
raw: Buffer(2126) [Uint8Array] [
48, 130, 8, 74, 48, 130, 7, 207, 160, 3, 2, 1,
2, 2, 16, 12, 116, 93, 202, 229, 63, 89, 16, 59,
237, 162, 71, 124, 204, 231, 58, 48, 10, 6, 8, 42,
134, 72, 206, 61, 4, 3, 3, 48, 86, 49, 11, 48,
9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 21,
48, 19, 6, 3, 85, 4, 10, 19, 12, 68, 105, 103,
105, 67, 101, 114, 116, 32, 73, 110, 99, 49, 48, 48,
46, 6, 3, 85, 4, 3, 19, 39, 68, 105, 103, 105,
67, 101, 114, 116,
... 2026 more items
],
issuerCertificate: {
subject: [Object: null prototype] {
C: 'US',
O: 'DigiCert Inc',
CN: 'DigiCert TLS Hybrid ECC SHA384 2020 CA1'
},
issuer: [Object: null prototype] {
C: 'US',
O: 'DigiCert Inc',
OU: 'www.digicert.com',
CN: 'DigiCert Global Root CA'
},
infoAccess: [Object: null prototype] {
'OCSP - URI': [ 'http://ocsp.digicert.com' ],
'CA Issuers - URI': [ 'http://cacerts.digicert.com/DigiCertGlobalRootCA.crt' ]
},
ca: true,
bits: 384,
pubkey: Buffer(97) [Uint8Array] [
4, 193, 27, 198, 154, 91, 152, 217, 164, 41, 160, 233,
212, 4, 181, 219, 235, 166, 178, 108, 85, 192, 255, 237,
152, 198, 73, 47, 6, 39, 81, 203, 191, 112, 193, 5,
122, 195, 177, 157, 135, 137, 186, 173, 180, 19, 23, 201,
168, 180, 131, 200, 184, 144, 209, 204, 116, 53, 54, 60,
131, 114, 176, 181, 208, 247, 34, 105, 200, 241, 128, 196,
123, 64, 143, 207, 104, 135, 38, 92, 57, 137, 241, 77,
145, 77, 218, 137, 139, 228, 3, 195, 67, 229, 191, 47,
115
],
asn1Curve: 'secp384r1',
nistCurve: 'P-384',
valid_from: 'Apr 14 00:00:00 2021 GMT',
valid_to: 'Apr 13 23:59:59 2031 GMT',
fingerprint: 'AE:C1:3C:DD:5E:A6:A3:99:8A:EC:14:AC:33:1A:D9:6B:ED:BB:77:0F',
fingerprint256: 'F7:A9:A1:B2:FD:96:4A:3F:26:70:BD:66:8D:56:1F:B7:C5:5D:3A:A9:AB:83:91:E7:E1:69:70:2D:B8:A3:DB:CF',
fingerprint512: 'A9:0D:FF:FB:4B:1C:A3:01:3F:B2:D2:78:3F:AB:A7:B8:03:1E:25:08:08:19:28:63:76:D4:12:EB:97:D3:A5:66:2D:C0:5D:4E:C4:0A:77:29:89:72:0D:F8:2A:7B:67:92:65:56:6D:13:75:F0:0C:85:50:C6:83:03:B8:6A:C0:35',
ext_key_usage: [ '1.3.6.1.5.5.7.3.1', '1.3.6.1.5.5.7.3.2' ],
serialNumber: '07F2F35C87A877AF7AEFE947993525BD',
raw: Buffer(1051) [Uint8Array] [
48, 130, 4, 23, 48, 130, 2, 255, 160, 3, 2, 1,
2, 2, 16, 7, 242, 243, 92, 135, 168, 119, 175, 122,
239, 233, 71, 153, 53, 37, 189, 48, 13, 6, 9, 42,
134, 72, 134, 247, 13, 1, 1, 12, 5, 0, 48, 97,
49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85,
83, 49, 21, 48, 19, 6, 3, 85, 4, 10, 19, 12,
68, 105, 103, 105, 67, 101, 114, 116, 32, 73, 110, 99,
49, 25, 48, 23, 6, 3, 85, 4, 11, 19, 16, 119,
119, 119, 46, 100,
... 951 more items
],
issuerCertificate: <ref *1> {
subject: [Object: null prototype] {
C: 'US',
O: 'DigiCert Inc',
OU: 'www.digicert.com',
CN: 'DigiCert Global Root CA'
},
issuer: [Object: null prototype] {
C: 'US',
O: 'DigiCert Inc',
OU: 'www.digicert.com',
CN: 'DigiCert Global Root CA'
},
ca: true,
modulus: 'E23BE11172DEA8A4D3A357AA50A28F0B7790C9A2A5EE12CE965B010920CC0193A74E30B753F743C46900579DE28D22DD870640008109CECE1B83BFDFCD3B7146E2D666C705B37627168F7B9E1E957DEEB748A308DAD6AF7A0C3906657F4A5D1FBC17F8ABBEEE28D7747F7A78995985686E5C23324BBF4EC0E85A6DE370BF7710BFFC01F685D9A844105832A97518D5D1A2BE47E2276AF49A33F84908608BD45FB43A84BFA1AA4A4C7D3ECF4F5F6C765EA04B37919EDC22E66DCE141A8E6ACBFECDB3146417C75B299E32BFF2EEFAD30B42D4ABB74132DA0CD4EFF881D5BB8D583FB51BE84928A270DA3104DDF7B216F24C0A4E07A8ED4A3D5EB57FA390C3AF27',
bits: 2048,
exponent: '0x10001',
pubkey: Buffer(294) [Uint8Array] [
48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134,
247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0,
48, 130, 1, 10, 2, 130, 1, 1, 0, 226, 59, 225,
17, 114, 222, 168, 164, 211, 163, 87, 170, 80, 162, 143,
11, 119, 144, 201, 162, 165, 238, 18, 206, 150, 91, 1,
9, 32, 204, 1, 147, 167, 78, 48, 183, 83, 247, 67,
196, 105, 0, 87, 157, 226, 141, 34, 221, 135, 6, 64,
0, 129, 9, 206, 206, 27, 131, 191, 223, 205, 59, 113,
70, 226, 214, 102,
... 194 more items
],
valid_from: 'Nov 10 00:00:00 2006 GMT',
valid_to: 'Nov 10 00:00:00 2031 GMT',
fingerprint: 'A8:98:5D:3A:65:E5:E5:C4:B2:D7:D6:6D:40:C6:DD:2F:B1:9C:54:36',
fingerprint256: '43:48:A0:E9:44:4C:78:CB:26:5E:05:8D:5E:89:44:B4:D8:4F:96:62:BD:26:DB:25:7F:89:34:A4:43:C7:01:61',
fingerprint512: '53:B4:44:E5:65:18:32:01:A6:1E:EB:46:12:09:B2:DC:30:89:5E:EC:A4:87:23:8D:15:A0:26:73:5F:22:9A:81:9E:5B:19:CB:D7:E2:FA:27:68:AB:2A:64:F6:EB:CD:9D:1E:72:13:41:C9:ED:5D:D0:9F:C0:D5:E4:3D:68:BC:A7',
serialNumber: '083BE056904246B1A1756AC95991C74A',
raw: Buffer(947) [Uint8Array] [
48, 130, 3, 175, 48, 130, 2, 151, 160, 3, 2, 1,
2, 2, 16, 8, 59, 224, 86, 144, 66, 70, 177, 161,
117, 106, 201, 89, 145, 199, 74, 48, 13, 6, 9, 42,
134, 72, 134, 247, 13, 1, 1, 5, 5, 0, 48, 97,
49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85,
83, 49, 21, 48, 19, 6, 3, 85, 4, 10, 19, 12,
68, 105, 103, 105, 67, 101, 114, 116, 32, 73, 110, 99,
49, 25, 48, 23, 6, 3, 85, 4, 11, 19, 16, 119,
119, 119, 46, 100,
... 847 more items
],
issuerCertificate: [Circular *1]
}
}
}
}
}
Expected Behavior
The request should succeed without SSL errors when using requestTls and the DNS interceptor together.
Environment
Ubuntu 22.04.5 LTS x86_64 v20.12.2 undici 7.2.0
Additional context
Maybe regression #3817 #3437
This issue occurs whether requestTls is { rejectUnauthorized: true } or simply {}. It appears the combination of requestTls and interceptors.dns() triggers unexpected behavior.
Dumped connection info using MITMProxy
When debugging an issue, I noticed that the value of requestedPath differs when the issue is present compared to when everything works correctly. Specifically, instead of containing the expected domain name, requestedPath contains the IP address resolved by DNS.
For example:
Expected: google.com:443
Actual: 1.1.1.1:443
This behavior occurs at the following line in the code: https://github.com/nodejs/undici/blob/54c5c6827c7d3f801bd72926234243a9ab8f7ef5/lib/dispatcher/proxy-agent.js#L74
Additional Context
While trying to gather more context about this issue, I found that debugging takes significant time. However, the key observation is the unexpected substitution of the domain name with an IP address in requestedPath.