当代理响应包含“连接:关闭”header 时如何处理 ECONNRESET?

How to handle ECONNRESET when proxied response includes `connection: close` header?

我正在尝试实施 MITM 代理。

我正在处理一个 CONNECT 请求,然后用于创建与内部 HTTPS 服务器的连接。

根据请求,HTTPS 服务器响应:

connection: close
foo

我希望客户端收到响应并代理关闭连接套接字。

相反,client 收到响应并且代理服务器记录错误:

server socket error Error: This socket has been ended by the other party
    at Socket.writeAfterFIN [as write] (net.js:407:14)
    at Socket.ondata (_stream_readable.js:713:22)
    at Socket.emit (events.js:200:13)
    at addChunk (_stream_readable.js:294:12)
    at readableAddChunk (_stream_readable.js:275:11)
    at Socket.Readable.push (_stream_readable.js:210:10)
    at TCP.onStreamRead (internal/stream_base_commons.js:166:17) {
  code: 'EPIPE'
}
request socket error Error: read ECONNRESET
    at TCP.onStreamRead (internal/stream_base_commons.js:183:27) {
  errno: 'ECONNRESET',
  code: 'ECONNRESET',
  syscall: 'read'
}

这是主题脚本:

const net = require('net');
const http = require('http');
const https = require('https');

const sslCertificate = {
  ca: '-----BEGIN CERTIFICATE REQUEST-----\n' +
    'MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B\n' +
    'AQEFAAOCAQ8AMIIBCgKCAQEAuftLzDyJ8dRk71pZ3637tCIZCVLJieLqIlAf7wT5\n' +
    '+qesTgu6vWzndZ4ze2V2lkac0xqFlW1djKT9IPUTCPx5dmWdT8mYFNUqB87hRWx9\n' +
    '6Ge21bs+KDppujHYrrgNjT8L3+RlHenoG7Qi5WuSzfOqP5nqCyoKFFNHJ0Ds52Uk\n' +
    'uvmTLzY/+kx3tFFGi4QXyva3T38uF99D4C2Tqxy7aRHEBJATQYxJgVPResiv31zv\n' +
    'qd6H1jYIZGw5s4QJFh5C7VXsoHs1dLIfDoNcV/fO95VQ+wXPxrl8mcVQzNV7RKmX\n' +
    'VHKudzx49IvOpRyM3OmN3RV5snOYKGmgwXQUF7JL2VSrSQIDAQABoAAwDQYJKoZI\n' +
    'hvcNAQELBQADggEBAIaUryumwXIxMJErT/7B46l2k27+xefaTPCddjERhqk8WH/N\n' +
    '95/yhvdzq1i0BSLv74Kh7L68kJiN8vtF6sAORofw42LMo+KzRDE1m1Zl7CVWw2DF\n' +
    'wT7SJov22t6dVx6HOcsZZSo5lSN+CMN3xkgt6jyEPbCKfCJzl44Y3eOpqzry6/GM\n' +
    'U+hR7nQx3IJmpAHNd7wolRzkf1X0gTifR5iC5S72GSRM9AnLfL2L0zQC6LmcNmZp\n' +
    '3deNxIC+w5kTALREiMq3P9McBMCgwRinOJLbhmV9ifPRpLa9e+mFVdHzbR7+09kp\n' +
    '6eNS19RndbHn6N1RbgFSNjDz28fMXISSWZFB/X4=\n-----END CERTIFICATE ' +
    'REQUEST-----',
  cert: '-----BEGIN CERTIFICATE-----\n' +
    'MIICpDCCAYwCCQCK9kDE6/eFXDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls\n' +
    'b2NhbGhvc3QwHhcNMTkwNzE5MTczMzI2WhcNMjAwNzE4MTczMzI2WjAUMRIwEAYD\n' +
    'VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5\n' +
    '+0vMPInx1GTvWlnfrfu0IhkJUsmJ4uoiUB/vBPn6p6xOC7q9bOd1njN7ZXaWRpzT\n' +
    'GoWVbV2MpP0g9RMI/Hl2ZZ1PyZgU1SoHzuFFbH3oZ7bVuz4oOmm6MdiuuA2NPwvf\n' +
    '5GUd6egbtCLla5LN86o/meoLKgoUU0cnQOznZSS6+ZMvNj/6THe0UUaLhBfK9rdP\n' +
    'fy4X30PgLZOrHLtpEcQEkBNBjEmBU9F6yK/fXO+p3ofWNghkbDmzhAkWHkLtVeyg\n' +
    'ezV0sh8Og1xX9873lVD7Bc/GuXyZxVDM1XtEqZdUcq53PHj0i86lHIzc6Y3dFXmy\n' +
    'c5goaaDBdBQXskvZVKtJAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGxXxytrNtm+\n' +
    'q4NpWtKhy3DL5LOMH+K8lqgJ29SmmDEcqWgevpUnqLYFvb3AOxU/vYId5rFmHb5A\n' +
    'WnXyKJ/YYSpNi47EcV+AJCwqDqBgAM4J3Tiiu6BguZ4sU20ZVFl1oQvTlQw8InLI\n' +
    'D1ciwwtgWS2z9pRKmQ2ar2TY+2yhnl0L1WCl50XH6PngzzEHSxHiPDnOYPyXQjPs\n' +
    'vkoJDmdnAVfWs2DfKfM0l27nIL2IBZr6Gks+nLwaK7FedQVD8ORYg9x/mwXO1oDr\n' +
    'sLyCQUlXhhBNBmn+TTLFPbrXetOU6le7iW3JJVMUv84vh8cV8aLtXDuQ0qlKMd8B\n' +
    'Mrgha3mM8EM=\n-----END CERTIFICATE-----',
  key: '-----BEGIN RSA PRIVATE KEY-----\n' +
    'MIIEpAIBAAKCAQEAuftLzDyJ8dRk71pZ3637tCIZCVLJieLqIlAf7wT5+qesTgu6\n' +
    'vWzndZ4ze2V2lkac0xqFlW1djKT9IPUTCPx5dmWdT8mYFNUqB87hRWx96Ge21bs+\n' +
    'KDppujHYrrgNjT8L3+RlHenoG7Qi5WuSzfOqP5nqCyoKFFNHJ0Ds52UkuvmTLzY/\n' +
    '+kx3tFFGi4QXyva3T38uF99D4C2Tqxy7aRHEBJATQYxJgVPResiv31zvqd6H1jYI\n' +
    'ZGw5s4QJFh5C7VXsoHs1dLIfDoNcV/fO95VQ+wXPxrl8mcVQzNV7RKmXVHKudzx4\n' +
    '9IvOpRyM3OmN3RV5snOYKGmgwXQUF7JL2VSrSQIDAQABAoIBAGnWDuFwBhQ/iR0I\n' +
    'rqJy0Q1GZjb/DL/SCOlz7WhIzbUNnClh1WgcxG8TkzqCmASWtIIR0rkhXp49+eq6\n' +
    'bJWtj7WHyAjysQAR+nQtD9dBETmjY9GnV4zvCOGzohpzlQqvOSO1RrHKPZMeZMln\n' +
    '+UgIhPbisOSfjNLaPWCiOu7HiSp5CgT70mSrylNQWhIa/okt8zjDbpV4QGPYP8J/\n' +
    'fi4k3u5C8oHwCt3DYp4Qc6ybKiMuBELVcoI0Ug0CtVriB11uNCYOqMbanj4VfRzq\n' +
    'KPTDRtkiF+EYi0PBstW+X9p7rFVB1PaBSF3PxudWMTmNZ1MooqOfkIves/T7YoxA\n' +
    'Uh9XUIECgYEA4Y8RU+/lf5GMDKstdwm+OH0NBOT/mrsFAlWnGtQivWddyPxvPVJH\n' +
    'LqIYtpqTH2luh7ksTcmTacqRjFx/ebobFAVgvg1zhCzHIdmgedGuxzvhEic1KRgT\n' +
    'EJgm4kW9uPFZugd05873uWf0cYjbQZXQhhn1E3bTorTuJJZoJu0R7ZECgYEA0xTb\n' +
    'bnFyOgD+c0A+kkirHiYU5RDAvtCS0jyKbZAPTP3fX016JeC2pxQcN4iLvgumm+Iv\n' +
    'ugtdrHYDzZTIzMl0pT8HSDqjaW8nNmEMvYaE8FYGlFHqEJQlweGMYeXdCxZSA+1D\n' +
    'HAzG8tW0rniMZp6KevZt5GCmBX3q0mH9ZKU/ZjkCgYEA14JTgwhOFXHiBuSyvu6v\n' +
    'MdfBTbDiy1rvMUjXLZoMSz1s7TDLtCJd4p97z1SnRzb8JW92dign0cd7A0oJfiuj\n' +
    '3aA5y7ycZ2hFJwGBA4OlY7TBmg+eClJ3PL6zQDR0TjVDjqu7NhSYuiwp8SRaoTJc\n' +
    'FxTMBTnegbIvawPOJYsTOxECgYEAlVDzyLTHsPBzDuQrXx+4rKMTtNadAl5Y/g+F\n' +
    'fOujZztPgAM2nQTRMG+xZjdZYx6qxSrDyD+yDAWPuyW8xeDceuiTJi0U28idXIJa\n' +
    'mNdHwxuXm+Q2R3QFIZmDzNzl+KnZap20E2uWcMFsBt+PsigEneck5aDY0Jm6OwjG\n' +
    'TyP2LUECgYALK+5AoQYbeUwVd3MhJONl0EdtKzjDq2wI127oXCjqVIe9BoqNedDu\n' +
    'zOvo5QjNApRbPZcaJB7e/3XbMFv/jSpeL9jC/AynGQBdpk3meL9KtC7Nm4wwj8XX\n' +
    'Ad5ZZkUZLAukbH1BqBuEgFjv3SDJ2g/aqUdqVfwq6qNSNdWzZTQG4w==\n-----END RSA ' +
    'PRIVATE KEY-----'
};

const handleConnect = (port, request, requestSocket, head) => {
  const {
    httpVersion
  } = request;

  const serverSocket = net.connect({
    port
  }, () => {
    requestSocket.write(
      'HTTP/' + httpVersion + ' 200 Connection established\r\n' +
      '\r\n'
    );

    serverSocket.write(head);

    serverSocket.pipe(requestSocket);
    requestSocket.pipe(serverSocket);
  });

  serverSocket.on('error', (error) => {
    console.log('server socket error', error);
  });

  requestSocket.on('error', (error) => {
    console.log('request socket error', error);
  });
};

const requestHandler = (incomingMessage, outgoingMessage) => {
  outgoingMessage.writeHead(200, {
    connection: 'close'
  });
  outgoingMessage.end(Buffer.from('foo'));
};

const main = () => {
  const httpServer = http.createServer(requestHandler);

  const internalHttpsServer = https
    .createServer(sslCertificate, requestHandler)
    .listen()
    .unref();

  httpServer.on('connect', (request, requestSocket, head) => {
    handleConnect(
      internalHttpsServer.address().port,
      request,
      requestSocket,
      head
    );
  });

  httpServer.listen(8080);
};

main();

此脚本可以使用任何 HTTPS URL 进行测试,例如

curl --proxy http://127.0.0.1:8080 'https://127.0.0.1/' -k

或者,您可以:

git clone https://github.com/gajus/http-proxy-connection-close.git
cd ./http-proxy-connection-close
node ./server.js
curl --proxy http://127.0.0.1:8080 'https://127.0.0.1/' -k

当代理响应包含 connection: close header 时如何处理 ECONNRESET 错误?

有两个不相关的问题。

可以通过在 "end" 事件上明确销毁客户端套接字来防止 ECONNRESET

serverSocket.once('end', () => {
  clientSocket.destroy();
});

然而,在对这个实现进行压力测试后,我开始看到 ECONNREFUSED 个错误。

原来有时IPC连接会失败:

errno:   ECONNREFUSED
code:    ECONNREFUSED
syscall: connect
address: /tmp/raygun-cjyaj51hz000035h3erhlf7a3.sock
name:    Error
message: connect ECONNREFUSED /tmp/raygun-cjyaj51hz000035h3erhlf7a3.sock
stack:
  """
    Error: connect ECONNREFUSED /tmp/raygun-cjyaj51hz000035h3erhlf7a3.sock
        at PipeConnectWrap.afterConnect [as oncomplete] (net.js:1054:14)
  """

这主要发生在 CPU 使用率接近 100% 时。

在那些情况下,客户端套接字将挂起等待与内部 HTTPS 服务器的连接。

解决方案是以下两种之一:

  1. 终止套接字
  2. 重试与内部 HTTPS 服务器的连接

这是前者的样子:

serverSocket.on('error', (error) => {
  log.error({
    error: serializeError(error)
  }, 'server socket error');

  clientSocket.write([
    'HTTP/1.1 503 Service Unavailable',
    'connection: close'
  ].join('\n') + '\n\n');

  clientSocket.end();
});

https://gist.github.com/gajus/72270a9f3aea3b09d61b997f7e5537f3