-
Notifications
You must be signed in to change notification settings - Fork 655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
grpc-js: initiate tls connection through http proxy #1369
Conversation
packages/grpc-js/src/http_proxy.ts
Outdated
realTarget, | ||
var cts = tls.connect({ | ||
...connectionOptions, | ||
host: options.host, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
options.host
here is the IP address of the proxy, and when socket
is set, host
is only used to validate the server certificates. Why would you want to validate the server certificates against the proxy's IP address?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. Also works without setting host
- have pushed update that removes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I think you want host: getDefaultAuthority(realTarget)
here, so that you don't have to pass the grpc.ssl_target_name_override
option when constructing the client. In fact, TLS certificate validation is the whole reason we pass the default authority to http2.connect
in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
packages/grpc-js/src/subchannel.ts
Outdated
@@ -302,21 +302,20 @@ export class Subchannel { | |||
if (proxyConnectionResult.socket) { | |||
connectionOptions.socket = proxyConnectionResult.socket; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With your changes, this line should do nothing because the createConnection
option below takes precedence.
packages/grpc-js/src/subchannel.ts
Outdated
/* net.NetConnectOpts is declared in a way that is more restrictive | ||
* than what net.connect will actually accept, so we use the type | ||
* assertion to work around that. */ | ||
return net.connect(this.subchannelAddress); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing this unconditionally breaks the unproxied TLS case, because you are forcing the use of a plaintext connection here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not worry about this for the moment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have updated this so that we still have the plaintext conditional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have the exact same createConnection
function in the secure and insecure case. The problem was, and is, that you're always doing net.connect
, which creates a plain TCP connection. In the secure case, you should only be using the createConnection
to pass in the socket
from the proxy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section of the change now doesn't functionally change anything, so I'd rather not change it at all.
Do I need to do something special to get all of the tests to pass? I get the following results:
This is the same result I get in master branch. |
You should undo that last commit. Using the target name override there is correct, that option just shouldn't be necessary in most cases. In general, when using the |
I've been making these small corrections, but overall I still don't understand how this change could cause any difference in behavior from what the existing code does. It looks to me like it is simply pulling out part of the behavior of |
To be honest, I as hoping you'd be able to explain it! Somehow this forces the tls connection to the real target to be established. |
I've pushed another update to this. I think this resolves the issues in your previous comments. My basic understanding is that when you initially set up the connection to the proxy it doesn't support tls, but if the proxy is connecting to a TLS server the socket needs upgrading to a TLS connection. Does that makes sense fro your perspective? |
The thing is that |
I know in theory that is supposed to be the case, but in practice it doesn't. Or something about the way we are calling |
packages/grpc-js/src/subchannel.ts
Outdated
connectionOptions.socket = proxyConnectionResult.socket; | ||
return proxyConnectionResult.socket; | ||
} else if ('secureContext' in connectionOptions) { | ||
return tls.connect(this.subchannelAddress); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't better because you're not passing in the options. When connecting with TLS and without a proxy, it should simply not be passing a createConnection
option at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My typescript is not up to par :( What I really wanted to do in the ssl block was like this:
if (proxyConnectionResult.socket) {
connectionOptions.createConnection = (authority, option) => {
return proxyConnectionResult.socket;
};
}
But I get typescript errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, the issue is that TypeScript thinks that proxyConnectionResult.socket
might become null
between when you check it and when the callback is called. This should work:
if (proxyConnectionResult.socket) {
connectionOptions.createConnection = (authority, option) => {
return proxyConnectionResult.socket!; //Exclamation mark here
};
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one! updated.
I'm not talking about "in theory", I'm talking about the |
Understood - but in reality it simply doesn't work the way it was. |
So, one difference between what you're doing here and what |
Yes, the tor connection works correctly even after wrapping my |
If I call like this it still works: const cts = tls.connect(Number(options.port), getDefaultAuthority(realTarget), initializeTLSOptions({ ...connectionOptions, socket}, getDefaultAuthority(realTarget)), () => {
resolve({ socket: cts, realTarget });
}); So wrapping the options in |
I have another idea: log calls to const originalTlsConnect = tls.connect;
tls.connect = (...args) => {
console.log(args);
return originalTlsConnect(...args);
}; Then check what the output is for your test case, both with this change (including the version with |
My version (works): [
9065,
'zapn34qfeedw2l5y26p3hnnkusqnbhxcxw64lq5cojmvq45yw4bc3sqd.onion',
{
secureContext: SecureContext { context: SecureContext {} },
socket: Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: [ReadableState],
readable: true,
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
_writableState: [WritableState],
writable: true,
allowHalfOpen: false,
_sockname: null,
_pendingData: null,
_pendingEncoding: '',
server: null,
_server: null,
parser: null,
_httpMessage: null,
[Symbol(asyncId)]: 133,
[Symbol(kHandle)]: [TCP],
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0
},
settings: {},
Http1IncomingMessage: [Function: IncomingMessage],
Http1ServerResponse: [Function: ServerResponse],
Http2ServerRequest: [Function: Http2ServerRequest],
Http2ServerResponse: [Function: Http2ServerResponse],
ALPNProtocols: [ 'h2' ],
servername: 'zapn34qfeedw2l5y26p3hnnkusqnbhxcxw64lq5cojmvq45yw4bc3sqd.onion'
},
[Function]
] Old version (doesn't work): [
'443',
'zapn34qfeedw2l5y26p3hnnkusqnbhxcxw64lq5cojmvq45yw4bc3sqd.onion',
{
secureContext: SecureContext { context: SecureContext {} },
socket: Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: [ReadableState],
readable: true,
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
_writableState: [WritableState],
writable: true,
allowHalfOpen: false,
_sockname: null,
_pendingData: null,
_pendingEncoding: '',
server: null,
_server: null,
parser: null,
_httpMessage: null,
[Symbol(asyncId)]: 135,
[Symbol(kHandle)]: [TCP],
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0
},
host: '127.0.0.1',
port: 9065,
settings: {},
Http1IncomingMessage: [Function: IncomingMessage],
Http1ServerResponse: [Function: ServerResponse],
Http2ServerRequest: [Function: Http2ServerRequest],
Http2ServerResponse: [Function: Http2ServerResponse],
ALPNProtocols: [ 'h2' ],
servername: 'zapn34qfeedw2l5y26p3hnnkusqnbhxcxw64lq5cojmvq45yw4bc3sqd.onion'
}
] Different host and port |
OK, it should be pretty simple to change that: in the unmodified code, just put this line in a |
Unfortunately not. I have also tried other things to make the |
Just to verify in the other direction, if you modify your version of the code to pass the same arguments that the old code passes, your version still works? |
Yes, it does. i.e. I can change to this and it still works:
Which gives me call opts that look like those from the original code: [
443,
'zapn34qfeedw2l5y26p3hnnkusqnbhxcxw64lq5cojmvq45yw4bc3sqd.onion',
{
secureContext: SecureContext { context: SecureContext {} },
socket: Socket {
connecting: false,
_hadError: false,
_parent: null,
_host: null,
_readableState: [ReadableState],
readable: true,
_events: [Object: null prototype] {},
_eventsCount: 0,
_maxListeners: undefined,
_writableState: [WritableState],
writable: true,
allowHalfOpen: false,
_sockname: null,
_pendingData: null,
_pendingEncoding: '',
server: null,
_server: null,
parser: null,
_httpMessage: null,
[Symbol(asyncId)]: 134,
[Symbol(kHandle)]: [TCP],
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0
},
host: '127.0.0.1',
port: 9065,
settings: {},
Http1IncomingMessage: [Function: IncomingMessage],
Http1ServerResponse: [Function: ServerResponse],
Http2ServerRequest: [Function: Http2ServerRequest],
Http2ServerResponse: [Function: Http2ServerResponse],
ALPNProtocols: [ 'h2' ],
servername: 'zapn34qfeedw2l5y26p3hnnkusqnbhxcxw64lq5cojmvq45yw4bc3sqd.onion'
},
[Function]
] There is a different though, which is the callback function as the 4th arg. It seems to me that in my version when calling In http2 core they don't call They will however call a However we don't pass in a `listener third arg. I may be barking up the wrong tree, but it seems that our socket is just not ready to use with tls before we start trying to use it. |
It should be pretty easy to check if passing a callback argument to In the other direction, what if in this PR you have the proxy code return the TLS socket immediately, instead of passing a callback? |
I think I may have a smoking gun: the If this is true, passing a callback to |
See nodejs/node#32922 for a demonstration of the problem I described independent of both |
Does't work this way.
Also does't work this way.
Wow ok, so this really does look like a bug in node itself. With that in mind, how do you feel about the workaround proposed in this PR? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I understand what the problem is, I'm OK with adding the workaround, with a few changes.
packages/grpc-js/src/subchannel.ts
Outdated
/* net.NetConnectOpts is declared in a way that is more restrictive | ||
* than what net.connect will actually accept, so we use the type | ||
* assertion to work around that. */ | ||
return net.connect(this.subchannelAddress); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section of the change now doesn't functionally change anything, so I'd rather not change it at all.
Thanks. Have made those updates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for working through this with me.
Thanks for your help - solving this and making it possible to do grpc over tor is a big deal. 👍 |
Unfortunately this no longer works after merging :( |
I think it's this PR that conflicts #1364 |
I think I fixed the conflict in #1375. |
Version 1.0.1 has that fix. |
progress, but still not working unfortunately. Now getting |
Trying to get ssl connection to establish correctly when connecting via proxy.
In reference to #992