Extend and customize listener behavior#329
Conversation
This patch allows listeners to: - be instantiated without binding to the specified port (config option bind_to_port) - have connections that have been redirected via iptables be handled by the listener associated with the original port rather than the listener that received the connection request. Use cases for these changes include: - Funnelling through the proxy connections for an entire range of destination port. If the proxy has a listener for the original destination port (before the redirect) it is used, otherwise the listener that received the connection handles it - Transparently proxy connections for an app listening on port X. The proxy can have an unbound listener for port X and a bound on for port Y. Connections originally directed to the app on port X are redirected with iptables to the proxy on port Y. The listener for port Y picks up the connection and hands it off to the listener for port X. Neither the app, nor the client, have to be changed or become aware of the presence of the proxy (except for the different src IP seen by the app). See https://docs.google.com/document/d/1v870Igrj5QS52G9O43fhxbV_S3mpvf_H6Hb8r85KZLY/ for the entire story
|
Some examples of the use of SO_ORIGINAL_DST in other proxies for comparison haproxy: redsocks: |
|
@enricoschiattarella at a high level this looks good. I will have some comments but in the meantime can you sign the CLA since that will take a little bit. |
|
SAMPLE USAGE / TESTING (1) Start simple ncat server on ports 1111 (HTTP) and 2222 (TCP):
(2) Program iptables to redirect all connections destined to ports 8081 and 8082 to port 12345:
(OUTPUT chain only applies to locally-initiated connections) (3) Start Envoy with below config. (4) Use curl to verify that connections established to ports 8081 and 8082 go through the proxy and finally make it to the actual service:
Note: it is possible for an unbound Envoy listener to use the same port the app is listening on. More details in the doc referenced in the commit message. |
| * @param stats_store supplies the Stats::Store to use. | ||
| * @param use_proxy_proto whether to use the PROXY Protocol V1 | ||
| * (http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt) | ||
| * @param bind_to_port specifies if the listener should actually bind to the port. |
include/envoy/event/dispatcher.h
Outdated
| * (http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt) | ||
| * @param bind_to_port specifies if the listener should actually bind to the port. | ||
| * a listener that doesn't bind can only receive connections redirected from | ||
| * other listeners that use the use_orig_dst |
There was a problem hiding this comment.
"that set use_origin_dst to true"
| * @param socket supplies the socket to listen on. | ||
| * @param cb supplies the callbacks to invoke for listener events. | ||
| * @param stats_store supplies the Stats::Store to use. | ||
| * @param bind_to_port specifies if the listener should actually bind to the port. |
There was a problem hiding this comment.
nit: can you add use_proxy_proto param and change comment per above.
include/envoy/server/configuration.h
Outdated
|
|
||
| /** | ||
| * @return bool specifies whether the listener should actually listen on the port. | ||
| * a listener that doesn't listen on a port can only receive connections |
There was a problem hiding this comment.
nit: alignment (a under bool)
| namespace Network { | ||
|
|
||
| TcpListenSocket::TcpListenSocket(uint32_t port) : port_(port) { | ||
| TcpListenSocket::TcpListenSocket(uint32_t port, bool bindToPort) : port_(port) { |
| /** | ||
| * @return the socket supplied to the listener at construction time | ||
| */ | ||
| ListenSocket& socket(void) { return socket_; } |
source/common/network/utility.cc
Outdated
| socklen_t addr_len = sizeof(sockaddr_storage); | ||
| int status = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, orig_addr, &addr_len); | ||
|
|
||
| return (status == 0) ? true : false; |
There was a problem hiding this comment.
nit: ? true : false is redundant. Just return (status == 0);
| std::string orig_sock_name = std::to_string( | ||
| listener->getAddressPort(reinterpret_cast<struct sockaddr*>(&orig_dst_addr))); | ||
|
|
||
| if (listener->socket_.name() != orig_sock_name) { |
There was a problem hiding this comment.
Is this to handle the case where someone uses SO_ORIGINAL_DST but actually means to send it to the current listener? If so can you put a comment to that effect?
| #include "common/event/dispatcher_impl.h" | ||
| #include "common/event/libevent.h" | ||
|
|
||
| #include "server/connection_handler.h" |
There was a problem hiding this comment.
The goal is to not cross streams here. Basically server depends on common but not the other way around. (No common code should include server code).
I would make an interface for the functionality you need and then use that from the server code.
There was a problem hiding this comment.
In this case I can actually get away with using a forward reference to avoid the dependency.
Otherwise, if ConnectionHandler abstract class becomes part of common, the issue goes away.
| listener->getAddressPort(reinterpret_cast<struct sockaddr*>(&orig_dst_addr))); | ||
|
|
||
| if (listener->socket_.name() != orig_sock_name) { | ||
| ListenerImpl* new_listener = |
There was a problem hiding this comment.
it's odd that we have to cast back from Listener to ListenerImpl. If so we should use dynamic_cast. With that said I think if we modify the interfaces correctly it should not be necessary.
There was a problem hiding this comment.
The main reason why this is "needed" is that there is no abstract class for ConnectionHandler. If the presence of a ConnectionHandler is an implementation detail that only ListenerImpl should know about (not Listener) then I need to cast. If it is ok to expose ConnectionHandler as a first-class component, then it can be part of the listener interface and there's no need to cast. We can even make the pointer to ConnectionHandler part of the Listener constructor and get rid of the connectionHandler(ConnectionHandler* conn_handler) function which I didn't really like.
There was a problem hiding this comment.
Yup let's add a first class ConnectionHandler interface which should be much cleaner.
|
All comments look straightforward except the one about downcasting Listener to ListenerImpl. |
|
I have implemented the interface for ConnectionHandler and solved the dependency violation between source/common and source/server. However, I haven't been able to get rid of the downcast from Listener to ListenerImpl in listener_impl.cc. The options I have considered are: both are reasonable in principle but have the drawback of adding low-level details about the connection (fd, sockaddr, ...) to interfaces. At this point the best solution seems to be the dynamic_cast<>, so I went with that. Please let me know if you have other alternatives in mind. Thanks! |
mattklein123
left a comment
There was a problem hiding this comment.
Change looks good. I'm fine with the dynamic cast now where you have it. Everything makes a lot more sense now. :)
Just a few small nits and I would like to move the interface into network namespace. Then we can get this in.
|
|
||
| bind_to_port | ||
| *(optional, boolean)* Whether the listener should bind to the port. A listener that doesn't bind | ||
| can only receive connections redirected from other listeners that set the use_origin_dst to |
There was a problem hiding this comment.
"the use_origin_dst parameter" (or no "the")
| #include "envoy/network/listen_socket.h" | ||
| #include "envoy/ssl/context.h" | ||
|
|
||
| namespace Server { |
There was a problem hiding this comment.
nit: Can we move this into network namespace. (Again not to cross streams since I don't think there is any need to). I would leave everything you did in the server implementation directory and just have it inherit from Network::ConnectionHandler
| /** | ||
| * Abstract connection handler. | ||
| */ | ||
|
|
| Event::DispatcherImpl& dispatcher_; | ||
| ListenSocket& socket_; | ||
| ListenerCallbacks& cb_; | ||
| bool bind_to_port_; |
There was a problem hiding this comment.
const bool on all the bool params (can you fix bind_to_port_ also)
|
|
||
| TcpListenSocket::TcpListenSocket(uint32_t port) : port_(port) { | ||
| TcpListenSocket::TcpListenSocket(uint32_t port, bool bind_to_port) : port_(port) { | ||
|
|
source/common/network/utility.cc
Outdated
| } | ||
|
|
||
| bool Utility::getOriginalDst(int fd, sockaddr_storage* orig_addr) { | ||
|
|
| @@ -1,5 +1,6 @@ | |||
| #pragma once | |||
|
|
|||
| #include "envoy/server/connection_handler.h" | |||
There was a problem hiding this comment.
nit: alpha order (for when you change to network dir)
|
|
||
| Api::Api& api() { return *api_; } | ||
| Event::Dispatcher& dispatcher() { return *dispatcher_; } | ||
| uint64_t numConnections() { return num_connections_; } |
There was a problem hiding this comment.
style nit:
We don't repeat comments for implementations (overrides), and put them at the end below other methods. We also put a small comment to organize the overrides. I will update the style guide with this info.
So it would go like:
class ConnectionHandlerImpl {
public:
void somethingNotInInterface();
// Network::ConnectionHandler
void method() override;
}
|
|
||
| #include "connection_handler.h" | ||
| #include "worker.h" | ||
| #include "connection_handler_impl.h" |
There was a problem hiding this comment.
keep alpha order if possible.
|
|
||
| MockConnectionHandler::MockConnectionHandler() {} | ||
| MockConnectionHandler::~MockConnectionHandler() {} | ||
| } // Server |
There was a problem hiding this comment.
nit: newline before this line
Regresion from envoyproxy#334. I'm in the process of rewriting all of the scope stuff currently so I'm not going to bother with tests. I will add tests in the real change.
… other review comments
|
@enricoschiattarella I think you have some other changes in this change. Do you mind merging master or just rebasing and pushing? I can then give it a final pass. |
|
@mattklein123 done. PTAL when you have time. Thanks! |
mattklein123
left a comment
There was a problem hiding this comment.
@enricoschiattarella tiny include order issue, otherwise looks great. If you could fix that I will merge in the morning when tests pass.
| #include "common/network/connection_impl.h" | ||
| #include "common/ssl/connection_impl.h" | ||
|
|
||
| #include "envoy/network/connection_handler.h" |
There was a problem hiding this comment.
can you put this up in "public" includes section (under envoy/common/exception.h)
There was a problem hiding this comment.
done. Thanks!
|
@enricoschiattarella, @mattklein123 The documentation says:
What is the desired behavior if the original listener has no filters like in the example above? I would expect the connection to be refused (no port matched from connecting client's perspective), but it seems the TCP connection is opened and then ... nothing. Should a listener with empty filter list and use_original_dst=true reject TCP connection altogether? Filters array is normally required (since listener configuration doesn't make sense without them) but an empty filter list doesn't throw an error. And in this special case it does make some sense to have no filter's specified since it only serves as a redirect. |
|
@ayj I think this is just a bug in general. If there are no filters, the connection handler should just close the connection. Feel free to fix or please open a GH issue and we can fix. |
Automatic merge from submit-queue. [DO NOT MERGE] Auto PR to update dependencies of mixerclient This PR will be merged automatically once checks are successful. ```release-note none ```
jwt_authn: implementation of www-authenticate header (envoyproxy#16216) (envoyproxy#17441)
Signed-off-by: LeiZhang <lei.a.zhang@intel.com>
Signed-off-by: Mike Schore <mike.schore@gmail.com> Signed-off-by: JP Simard <jp@jpsim.com>
Signed-off-by: Mike Schore <mike.schore@gmail.com> Signed-off-by: JP Simard <jp@jpsim.com>
**Commit Message** This upgrades the Go version to v1.24. There are a few things to note: * Starts utilizing testing.T.Context() in various places. * Adds `make clean` target in case local setup goes wrong. --------- Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
**Commit Message** Previously, the .bin directly was not cleared, so this fixes it. **Related Issues/PRs (if applicable)** Follow up on #329 Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
**Commit Message** Use t.Context instead of Background whenever possible. **Related Issues/PRs (if applicable)** Follow up on #329 Signed-off-by: Takeshi Yoneda <t.y.mathetake@gmail.com>
This patch allows listeners to:
listener associated with the original port rather than the listener
that received the connection request.
Use cases for these changes include:
If the proxy has a listener for the original destination port (before the redirect)
it is used, otherwise the listener that received the connection handles it
The proxy can have an unbound listener for port X and a bound on for port Y.
Connections originally directed to the app on port X are redirected with iptables
to the proxy on port Y. The listener for port Y picks up the connection and hands
it off to the listener for port X.
Neither the app, nor the client, have to be changed or become aware of the presence
of the proxy (except for the different src IP seen by the app).
See https://docs.google.com/document/d/1v870Igrj5QS52G9O43fhxbV_S3mpvf_H6Hb8r85KZLY/
for the entire story