Node.js net.lookUpAndConnect 버그 수정
일단 저번에 가상환경 밖에서 돌렸을 때 에러가 떴던 테스트의 에러 메세지부터 살펴보자.
node/test/parallel/test-net-socket-connect-without-cb.js
=== release test-net-socket-connect-without-cb ===
Path: parallel/test-net-socket-connect-without-cb
node:events:498
throw er; // Unhandled 'error' event
^
Error: getaddrinfo ENOTFOUND localhost
at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:109:26)
Emitted 'error' event on Socket instance at:
at emitErrorNT (node:internal/streams/destroy:170:8)
at emitErrorCloseNT (node:internal/streams/destroy:129:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
errno: -3007,
code: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'localhost'
}
node/test/parallel/test-tcp-wrap-listen.js
=== release test-tcp-wrap-listen ===
Path: parallel/test-tcp-wrap-listen
(node:377826) internal/test/binding: These APIs are for internal testing only. Do not use them.
(Use `node --trace-warnings ...` to show where the warning was created)
node:events:498
throw er; // Unhandled 'error' event
^
Error: getaddrinfo ENOTFOUND localhost
at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:109:26)
Emitted 'error' event on Socket instance at:
at emitErrorNT (node:internal/streams/destroy:170:8)
at emitErrorCloseNT (node:internal/streams/destroy:129:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
errno: -3007,
code: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'localhost'
}
localhost
에 연결을 시도하고 있는 것 같다. node/test/parallel/test-net-socket-connect-without-cb.js
코드에 로그를 찍어서 client.connect
함수에 뭔 인자가 넘어가는지 한 번 보자.
'use strict';
const common = require('../common');
// This test ensures that socket.connect can be called without callback
// which is optional.
const net = require('net');
const server = net.createServer(common.mustCall(function(conn) {
conn.end();
server.close();
})).listen(0, common.mustCall(function() {
const client = new net.Socket();
client.on('connect', common.mustCall(function() {
client.end();
}));
const address = server.address();
if (!common.hasIPv6 && address.family === 'IPv6') {
// Necessary to pass CI running inside containers.
client.connect(address.port);
} else {
console.log(address); // added
client.connect(address);
}
}));
Output
{address: '::', family: 'IPv6', port: 38065}
뭔가 이상하다. localhost
라는 DNS 주소가 아니라 ::
라는 IPv6 주소가 넘어가는데… 또 이상한 게 있는데, 아래 코드를 main
branch의 node로 실행시킨 후 nc :: 12345
명령어로 서버에 접속하면 접속이 잘 된다.
'use strict';
const net = require('net');
const server = net.createServer((conn) => {
conn.write("Hello World\n");
conn.end();
server.close();
}).listen({
host: '::',
port: 12345,
family: 'IPv6'
}, function () {
const client = new net.Socket();
client.on('connect', function () {
// send message and close
client.write("Hello World\n");
client.end();
});로
});
추가로, 테스트 코드의 에러 내용으로 구글링을 해 보니까 /etc/hosts
를 수정해 localhost
가 ::
주소로 resolve 될 수 있게 하면 된다고 하는데, 그 전에 Node.js에서 굳이 IP 주소가 아니라 DNS 주소로 연결을 시도하는 것 부터가 문제인 것 같아서 일단 놔두기로 했다.
node/lib/net.js
코드에 있는 net.Socket.connect
함수(node/lib/net.js
에서는 프로토타입 함수인 Socket.prototype.connect
)에 중단점을 열심히 찍어서 디버깅을 해 봤고, 함수의 마지막인 아래 부분 이전에서는 에러가 안 터지는 걸 찾았다.
if (pipe) {
validateString(path, 'options.path');
defaultTriggerAsyncIdScope(
this[async_id_symbol], internalConnect, this, path,
);
} else {
lookupAndConnect(this, options);
}
return this;
또한 node/test/parallel/test-net-socket-connect-without-cb.js
테스트 코드에서는 else 쪽으로 분기가 되고, lookupAndConnect
에 전달되는 options
인자가 테스트 코드의 client.connect(address);
와 같다는 것도 발견했다. 정확히는 address
가 경우에 따라 약간 전처리가 되고 lookupAndConnect
에 전달되는데, 지금의 경우에는 그대로 전달된다.
그래서 lookupAndConnect
에도 중단점을 계속 찍어가면서 에러가 발생하는 부분을 찾아봤다. 이 부분이었다. 정확히는 lookup
함수에서 에러가 발생한 후 콜백 함수인 emitLookup
에 에러 내용이 전달되고, process.nextTick
으로 throwing을 하는 것 같다.
defaultTriggerAsyncIdScope(self[async_id_symbol], function() {
lookup(host, dnsopts, function emitLookup(err, ip, addressType) {
self.emit('lookup', err, ip, addressType, host);
// It's possible we were destroyed while looking this up.
// XXX it would be great if we could cancel the promise returned by
// the look up.
if (!self.connecting) return;
if (err) {
// net.createConnection() creates a net.Socket object and immediately
// calls net.Socket.connect() on it (that's us). There are no event
// listeners registered yet so defer the error event to the next tick.
process.nextTick(connectErrorNT, self, err);
} else if (!isIP(ip)) {
err = new ERR_INVALID_IP_ADDRESS(ip);
process.nextTick(connectErrorNT, self, err);
} else if (addressType !== 4 && addressType !== 6) {
err = new ERR_INVALID_ADDRESS_FAMILY(addressType,
options.host,
options.port);
process.nextTick(connectErrorNT, self, err);
} else {
self._unrefTimer();
defaultTriggerAsyncIdScope(
self[async_id_symbol],
internalConnect,
self, ip, port, addressType, localAddress, localPort,
);
}
});
});
위의 코드 블럭에서 lookup
에 넘어가는 인자들을 보니까 host
는 'localhost'
였고, dnsopts
에는 IP 버전 정보와 DNS resolver 관련 정보가 들어있었다. 앞서 말했듯 DNS 주소인 localhost
가 IPv6 주소로 resolve되도록 hosts 파일을 설정하지 않아서 저런 인자가 넘어가면 에러가 나는 건 이상하지 않은데, 애초에 host
에 IP 주소 대신 'localhost'
가 들어가는 게 이상하다.
다시 lookupAndConnect
함수를 전체적으로 살펴봤다. 그리고 lookupAndConnect
에 인자로 넘어간 options
의 options.address
값을 함수 내부에서 전혀 이용하지 않는 것을 발견했다. 그 대신 server.address
의 리턴값에는 존재하지 않는 값인 options.host
를 사용해서 lookup
에 전달될 host
를 정의하고 있었다.
function lookupAndConnect(self, options) {
const { localAddress, localPort } = options;
const host = options.host || 'localhost';
let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options;
...
}
이제 이유는 알아냈으니까 host
값으로 options.address
도 사용할 수 있게 만들자. host
값으로 options.host
가 사용되는 것을 가정하고 만든 코드도 많을 테니까 위 코드에서 options.host
를 options.address
로 바꿔버리면 안 될 것이다. 대신 options.host
가 존재하지 않을 때만 options.address
값을 host
로 이용하고, options.address
값도 없을 때 이전처럼 host
를 'localhost'
로 만들면 될 것이다.
function lookupAndConnect(self, options) {
const { localAddress, localPort } = options;
let host = options.host;
if (!host) {
host = options.address;
}
if (!host) {
host = 'localhost';
}
let { port, autoSelectFamilyAttemptTimeout, autoSelectFamily } = options;
...
}
이러고 다시 bulid 후 test를 돌려보니 처음 고치기로 했던 test-net-socket-connect-without-cb
뿐만 아니라 node/test/parallel/test-tcp-wrap-listen.js
까지 고쳐졌고, 새로 발생한 에러는 없어서 모든 test를 통과했다.
이제 GitHub에 Issue랑 PR을 어떻게 올릴 지 좀 알아봐야 될 것 같다.
그나저나 저런 버그가 있으면 server.address()
의 리턴 값 형식으로 서버 주소를 알아왔을 때, localhost가 아닌 서버에 접속하면 항상 에러가 터질 텐데… 이게 어떻게 지금까지 남아있지.
Leave a comment