81

With the release of TCP load balancing for the Nginx community version, I would like to mix OpenVPN and SSL pass-through data. The only way for Nginx to know how to route the traffic is via their domain name.

 vpn1.app.com ─┬─► nginx at 10.0.0.1 ─┬─► vpn1  at 10.0.0.3
 vpn2.app.com ─┤                      ├─► vpn2  at 10.0.0.4
https.app.com ─┘                      └─► https at 10.0.0.5

I have taken a look at the TCP guides and the module documentation, but it doesn't seem well referenced. If anyone can point me to the right direction, i'd be grateful.

Related question on ServerFault: Can a Reverse Proxy use SNI with SSL pass through?

Paulo Boaventura
  • 1,365
  • 1
  • 9
  • 29
James Wong
  • 4,529
  • 4
  • 48
  • 65
  • is there anything missing or unclear from my answer? – cnst Jan 25 '16 at 19:24
  • -1 for failing to award the promised bounty even though a comprehensive and referenced answer was provided, and no extra clarifications were sought nor issues pointed out – cnst Jan 27 '16 at 02:58
  • 3
    thanks for finally accepting and upvoting my answer, however, i'm puzzled why within the same minute, some of my other unrelated and undisputed answers got downvoted for absolutely no apparent reason; is it a new thing to post a bounty, quietly fail to award it for no reason, then when it's pointed out, to silently downvote other answers of the very person who's helped you? – cnst Jan 29 '16 at 17:46
  • 1
    Stack Overflow is a site for programming and development questions. This question appears to be off-topic because it is not about programming or development. See [What topics can I ask about here](http://stackoverflow.com/help/on-topic) in the Help Center. Perhaps [Unix & Linux Stack Exchange](http://unix.stackexchange.com/) or [Information Security Stack Exchange](http://security.stackexchange.com/) would be a better place to ask. – jww Feb 06 '17 at 22:52
  • @jww Please flag the topic for moderation and help close it. – James Wong Feb 23 '17 at 10:03
  • @JamesWong do we need to have SSL on vpn1.app.com, if so can it be a wild card one ? – rMili Aug 16 '19 at 06:15

3 Answers3

111

This is now possible with the addition of the ngx_stream_ssl_preread module added in Nginx 1.11.5 and the ngx_stream_map module added in 1.11.2.

This allows Nginx to read the TLS Client Hello and decide based on the SNI extension which backend to use.

stream {

    map $ssl_preread_server_name $name {
        vpn1.app.com vpn1_backend;
        vpn2.app.com vpn2_backend;
        https.app.com https_backend;
        default https_default_backend;
    }

    upstream vpn1_backend {
        server 10.0.0.3:443;
    }

    upstream vpn2_backend {
        server 10.0.0.4:443;
    }

    upstream https_backend {
        server 10.0.0.5:443;
    }

    upstream https_default_backend {
        server 127.0.0.1:443;
    }

    server {
        listen 10.0.0.1:443;
        proxy_pass $name;
        ssl_preread on;
    }
}
Dave T.
  • 158
  • 1
  • 8
Lochnair
  • 1,566
  • 2
  • 10
  • 13
  • 50
    To whoever arrives here in the future thinking this could work for SSH connections - don't bother, SSH clients don't pass the DNS name. – Dae Mar 08 '17 at 08:39
  • 13
    @Dae, SSH is not even related to SSL. – cnst Aug 15 '17 at 21:37
  • in such case if I set up conventional https proxy listening on 127.0.0.1:443 I can chain stream with normal http proxy in nginx? For example to use nginx as vpn/httos reverse proxy but also as SSL offload proxy for https. – Lapsio Oct 04 '17 at 19:47
  • @Lapsio I'm a bit unclear on exactly what setup you're trying to achieve, but yes having a TCP stream in front of a HTTP web server works perfectly fine. – Lochnair Oct 05 '17 at 19:25
  • @Lochnair yeah i did it, hosting stream and http proxy on one machine works fine. Unfortunately though it seems OpenVPN client (at least RouterOS and NetworkManager on Linux implementations) don't forward hostname in SNI so I had to default to VPN and whitelist all https hostnames to route them to http proxy explicitly. And wildcard in map doesn't seem to work either :c – Lapsio Oct 06 '17 at 20:17
  • 2
    I had to set `proxy_protocol on;` to get this working, perhaps others need to do the same... – Carel Jan 17 '18 at 21:00
  • 1
    @Carel proxy_protocol is only needed here if you're using it on the upstream servers – Lochnair Jan 18 '18 at 17:23
  • Would it be possible to specify the source IP for each of the backend? – yegle Aug 02 '19 at 22:06
  • @Lochnair do we need to have SSL on vpn1.app.com, if so can it be a wild card one ? – rMili Aug 16 '19 at 06:13
  • 1
    @rMili You do, and yes it can. At this stage the certificates haven't even been sent yet, so it doesn't matter. – Lochnair Aug 24 '19 at 18:33
  • 1
    Even this answer looks good, it seems that OpenVpn client isn't sending the SNI, i've tried this approach, but i'm unable to make it work: https://stackoverflow.com/q/57713606/4175637 – Symon Aug 29 '19 at 15:45
  • @Lochnair I have http proxy in one upstream. It's working fine with your above config but in the proxy logs instead of logging the client ip address it logs the server ip. Can you help me how can i log client ip address? – Khakhar Shyam Jan 09 '20 at 06:32
  • It is very important to mention that the servers the upstreams are targeting must not use http2. Because if they do and share the same certificate (for example same wildcard certificate as my case was) then traffic that is supposed to reach another host will be sent through the same http2 connection/pipe. Causes – turbophi Jan 31 '20 at 23:11
  • 1
    I had to also do `load_module /usr/lib/nginx/modules/ngx_stream_module.so;` on top of the nginx conf file – xtrinch Jan 12 '21 at 18:25
  • How does one pass the hostname to the upstream for non-tls data? – Vinay Mundada Nov 22 '21 at 10:50
  • add a `hostnames;` line into the map to allow wildcard mappings ;) – xeruf May 02 '22 at 14:55
35

Assumptions

If I understand you correctly, you effectively want nginx to listen at a single IP address and TCP port combination (e.g., listen 10.0.0.1:443), and then, depending on the characteristic of the incoming TCP stream traffic, route it to one of the 3 different IP addresses.

You don't explicitly mention how you expect it to differentiate between the 3 different domains at stake, but my assumption is that you assume it's all just TLS, and must want to employ some sort of a TLS SNI (Server Name Indication) mechanism for domain-based differentiation.

I would believe that the stream-related documentation provided at http://nginx.org/docs/ is quite authoritative and exhaustive for the modules at stake (I'm listing all of it here, since apparently there's no central place for cross-referencing this yet, e.g., no references from the "stream core" module to the submodules yet (and docs/stream/ just redirects back docs/), which is indeed quite confusing, since stuff like http://nginx.org/r/upstream is only documented to apply to http, without any mention of applicability to stream, even if the directives are about the same in the end):


Answer

Note that each nginx directive, from each module, has a limited number of applicable Context's.

As such, unfortunately, there is simply no directive to snoop into SNI here!

To the contrary, it's actually documented in stream_core that, to quote, "Different servers must listen on different address:port pairs.", which, as you may note, is also contrary to how the listen directive works within the more-common http_core, and is a rather unambiguous reference to the fact that no kind of SNI support is presently implemented for the listen within stream.


Discussion

As a discussion point and a resolution suggestion, the assumption that OpenVPN traffic is just TLS with the snoopable SNI is also not necessarily correct (but I'm not too familiar with OpenSSL or SNI):

  • Consider that even if SNI is passively snoopable today, that's clearly contrary to the promise of TLS of keeping the connection secure, and, as such, may change in a future version of TLS.

  • For the sake of discussion, if OpenVPN is just using a TLS connection, and if it is NOT using TLS for authenticating users with user certificates (which would make it much more difficult to MitM the stream, yet still carry the authentication data all along), then, theoretically, if nginx did have SNI support around the listen within stream, then you'd possibly have been able to actively MitM it with nginx (since proxy_ssl is already supported in stream_proxy).

Most importantly, I believe OpenVPN may best be run over its own UDP-based protocol, in which case, you can use the same IP address and port number for one instance of the TCP-based https and another one of the UDP-based OpenVPN without a conflict.

In the end, you may ask, what would the stream module be useful for anyways, then? I believe its target audience would be, (0), load balancing HTTP/2 with multiple upstream servers, based on the hash of the IP-address of the client, for example, and/or, (1), a more straightforward and protocol-agnostic replacement for stunnel.

cnst
  • 25,870
  • 6
  • 90
  • 122
  • 1
    @TorstenBronger, I don't think people read the body of the question, which specifically deals with OpenVPN. Does OpenVPN itself even support SNI? – cnst Feb 06 '17 at 22:36
  • 1
    @TorstenBronger, BTW, it so turns out that OpenSSL does not support SNI, so, the question is moot, and thus the other answer is answering a different question. Source: http://stackoverflow.com/questions/42078600/if-applicable-does-openvpn-over-tcp-support-sni/42096540#42096540 – cnst Feb 07 '17 at 22:00
  • Once nginx has routed by hostname, OpenVPN doesn't need SNI anymore. – Torsten Bronger Feb 08 '17 at 05:22
  • 2
    This answer is now out of date. See the other answers. – Richard Kiefer Aug 02 '19 at 08:01
  • @RichardKiefer I have http proxy in one upstream. It's working fine with lochnair answer config but in the proxy logs instead of logging the client ip address it logs the server ip. Can you help me how can i log client ip address? – Khakhar Shyam Jan 09 '20 at 06:36
  • 1
    @KhakharShyam sorry, I am not fluent in that field. Consider posting a question regarding your specific problem. – Richard Kiefer Jan 09 '20 at 15:29
  • the best answer i found you all my study a lot of gratitude @cnst – Paulo Boaventura Dec 25 '20 at 21:17
  • There is a [good](https://www.nginx.com/blog/running-non-ssl-protocols-over-ssl-port-nginx-1-15-2/) introduction to stream/tcp from the nginx folks in a blog post. – Alex Apr 09 '21 at 04:57
10

AS @Lochnair mentioned, you can use ngx_stream_map module and variable $server_addr to resolve this problem. Here is my example.

My host IP is 192.168.168.22, and I use keepalived bound 2 virtual IP to eth0.

$sudo ip a
...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
link/ether 5c:f3:fc:b9:f0:84 brd ff:ff:ff:ff:ff:ff
inet 192.168.168.22/24 brd 192.168.168.255 scope global eth0
   valid_lft forever preferred_lft forever
inet 192.168.168.238/32 scope global eth0
   valid_lft forever preferred_lft forever
inet 192.168.168.239/32 scope global eth0
   valid_lft forever preferred_lft forever

$nginx -v
nginx version: nginx/1.13.2

$cat /etc/nginx/nginx.conf
...
stream {
    upstream pod53{
        server 10.1.5.3:3306;
    }
    upstream pod54{
        server 10.1.5.4:3306;
    }

    map $server_addr $x {
        192.168.168.238 pod53;
        192.168.168.239 pod54;
    }
    server {
        listen 3306;
        proxy_pass $x;
    }
}

Thus, I can visit different MySQL service with the same port 3306 via different VIPs. Just like visiting different HTTP service with the same port via diffrent server_name.

192.168.168.238 -> 10.1.5.3
192.168.168.239 -> 10.1.5.4
aloisio
  • 318
  • 4
  • 11
  • 6
    This example selects by IP address. This is not what the question was all about, where a selection by SNI name is required. Of course everything is much easier if you can afford a separate IPv4 address per HTTPS domain. – vog Feb 05 '19 at 23:02