Skip to content

Extend and customize listener behavior#329

Merged
mattklein123 merged 12 commits intoenvoyproxy:masterfrom
enricoschiattarella:master
Jan 12, 2017
Merged

Extend and customize listener behavior#329
mattklein123 merged 12 commits intoenvoyproxy:masterfrom
enricoschiattarella:master

Conversation

@enricoschiattarella
Copy link
Copy Markdown
Contributor

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

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
@enricoschiattarella
Copy link
Copy Markdown
Contributor Author

@mattklein123
Copy link
Copy Markdown
Member

@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.

@enricoschiattarella
Copy link
Copy Markdown
Contributor Author

enricoschiattarella commented Jan 6, 2017

SAMPLE USAGE / TESTING

(1) Start simple ncat server on ports 1111 (HTTP) and 2222 (TCP):

while true ; do ncat -l -p 1111 -c 'echo -e "HTTP/1.1 200 OK\n\n Hello from 1111"'; done
ncat -l 2222 -k -c echo "Hello from 2222"

(2) Program iptables to redirect all connections destined to ports 8081 and 8082 to port 12345:

sudo iptables -t nat -I OUTPUT 1 -p tcp --dport 8081 -j REDIRECT --to-port 12345
sudo iptables -t nat -I OUTPUT 1 -p tcp --dport 8082 -j REDIRECT --to-port 12345

(OUTPUT chain only applies to locally-initiated connections)

(3) Start Envoy with below config.

{
  "listeners": [
    {
      "port": 8081,
      "bind_to_port": false,
      "filters": [
        {
          "type": "read",
          "name": "http_connection_manager",
          "config": {
            "codec_type": "auto",
            "stat_prefix": "ingress_http",
            "route_config": {
              "virtual_hosts": [
                {
                  "name": "backend",
                  "domains": ["*"],
                  "routes": [
                    {
                      "timeout_ms": 0,
                      "prefix": "/",
                      "cluster": "service1"
                    }
                  ]
                }
              ]
            },
            "access_log": [
              {
                "path": "/tmp/access.envoy"
              }
            ],
            "filters": [
              {
                "type": "decoder",
                "name": "router",
                "config": {}
              }
            ]
          }
        }
      ]
    },
    {
      "port": 8082,
      "bind_to_port": false,
      "filters": [
        {
          "type": "read",
          "name": "tcp_proxy",
          "config": {
          "cluster": "service2",
          "stat_prefix": "tcp"
          }
        }
      ]
    },
    {
      "port": 12345,
      "use_original_dst": true,
      "filters": []
    }
  ],
  "admin": {
    "access_log_path": "/tmp/access.envoy",
    "port": 8001
  },
  "cluster_manager": {
    "clusters": [
      {
        "name": "service1",
        "connect_timeout_ms": 250,
        "type": "strict_dns",
        "lb_type": "round_robin",
        "hosts": [
          {
            "url": "tcp://localhost:1111"
          }
        ]
      },
      {
        "name": "service2",
        "connect_timeout_ms": 250,
        "type": "strict_dns",
        "lb_type": "round_robin",
        "hosts": [
          {
            "url": "tcp://localhost:2222"
          }
        ]
      }
    ]
  }
}

(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:

curl localhost:8081
Hello from 1111
curl localhost:8082
Hello from 2222

cat /tmp/envoy.access
[2017-01-06T17:40:50.474Z] "GET / HTTP/1.1" 200 - 0 17 3 2 "-" "curl/7.51.0-DEV" "6eb90d05-893d-4561-ba2b-be27ea111e50" "localhost:8081" "tcp://127.0.0.1:1111"

Note: it is possible for an unbound Envoy listener to use the same port the app is listening on.
In our example, the apps are listening on ports 1111 and 222; Envoy unbound listeners would also listen on 1111 and 2222, so that the proxying is transparent to the client. In that case, however, the iptables rules have to be tweaked, so that when the proxy tries to connect to the app on ports 1111 and 2222,
the connections are not intercepted and redirected back to the proxy.
This can be done by using the "-m owner" iptables option, which excludes connections originating from a process owned by a given UID/GID, using the PREROUTING chain, which is not traversed by locally generated packets, or using iptables MARKS.

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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit param doc order

* (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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you add use_proxy_proto param and change comment per above.


/**
* @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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: alignment (a under bool)

namespace Network {

TcpListenSocket::TcpListenSocket(uint32_t port) : port_(port) {
TcpListenSocket::TcpListenSocket(uint32_t port, bool bindToPort) : port_(port) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: bind_to_port

/**
* @return the socket supplied to the listener at construction time
*/
ListenSocket& socket(void) { return socket_; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: no (void)

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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup let's add a first class ConnectionHandler interface which should be much cleaner.

@enricoschiattarella
Copy link
Copy Markdown
Contributor Author

All comments look straightforward except the one about downcasting Listener to ListenerImpl.
Please let me know what is your take on that.
I will fix that and incorporate the others in the meanwhile.

@enricoschiattarella
Copy link
Copy Markdown
Contributor Author

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:
(1) add the newConnection method defined on the ListenerImpl to the Listener interface
(2) push the job of redirecting a connection to the ConnectionHandler by having a method like ConnectionHandler::redirect(connection, socket_name)

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!

Copy link
Copy Markdown
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the use_origin_dst parameter" (or no "the")

#include "envoy/network/listen_socket.h"
#include "envoy/ssl/context.h"

namespace Server {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: del new line

Event::DispatcherImpl& dispatcher_;
ListenSocket& socket_;
ListenerCallbacks& cb_;
bool bind_to_port_;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: del new line

}

bool Utility::getOriginalDst(int fd, sockaddr_storage* orig_addr) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: del new line

@@ -1,5 +1,6 @@
#pragma once

#include "envoy/server/connection_handler.h"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

override

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep alpha order if possible.


MockConnectionHandler::MockConnectionHandler() {}
MockConnectionHandler::~MockConnectionHandler() {}
} // Server
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: newline before this line

rshriram and others added 4 commits January 10, 2017 11:19
@mattklein123
Copy link
Copy Markdown
Member

@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.

@enricoschiattarella
Copy link
Copy Markdown
Contributor Author

@mattklein123 done. PTAL when you have time. Thanks!

Copy link
Copy Markdown
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you put this up in "public" includes section (under envoy/common/exception.h)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. Thanks!

@mattklein123 mattklein123 merged commit 164c2ad into envoyproxy:master Jan 12, 2017
@ayj
Copy link
Copy Markdown
Contributor

ayj commented Jan 20, 2017

@enricoschiattarella, @mattklein123

The documentation says:

If there is no listener associated with the original destination port,
the connection is handled by the [original] listener that receives it."

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.

@mattklein123
Copy link
Copy Markdown
Member

@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.

rshriram pushed a commit to rshriram/envoy that referenced this pull request Oct 30, 2018
rshriram pushed a commit to rshriram/envoy that referenced this pull request Oct 30, 2018
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
```
cyran-ryan pushed a commit to cyran-ryan/envoy that referenced this pull request Aug 11, 2021
liverbirdkte referenced this pull request in liverbirdkte/envoy Nov 17, 2022
Signed-off-by: LeiZhang <lei.a.zhang@intel.com>
jpsim pushed a commit that referenced this pull request Nov 28, 2022
Signed-off-by: Mike Schore <mike.schore@gmail.com>
Signed-off-by: JP Simard <jp@jpsim.com>
jpsim pushed a commit that referenced this pull request Nov 29, 2022
Signed-off-by: Mike Schore <mike.schore@gmail.com>
Signed-off-by: JP Simard <jp@jpsim.com>
mathetake added a commit that referenced this pull request Mar 3, 2026
**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>
mathetake added a commit that referenced this pull request Mar 3, 2026
**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>
mathetake added a commit that referenced this pull request Mar 3, 2026
**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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants