Skip to content

Commit 15db3c0

Browse files
Add support for PROXY protocol v2 in TCP listener (#13540) (#14404)
* Add support for PROXY protocol v2 in TCP listener I did not find tests for this so I added one trying to cover different configurations to make sure I did not break something. As far as I know, the behavior should be exactly the same as before except for one thing when proxy_protocol_behavior is set to "deny_unauthorized", unauthorized requests were previously silently reject because of https://github.com/armon/go-proxyproto/blob/7e956b284f0a/protocol.go#L81-L84 but it will now be logged. Also fixes #9462 by adding support for `PROXY UNKNOWN` for PROXY protocol v1. Closes #3807 * Add changelog Co-authored-by: Rémi Lapeyre <remi.lapeyre@lenstra.fr>
1 parent d848664 commit 15db3c0

File tree

6 files changed

+296
-21
lines changed

6 files changed

+296
-21
lines changed

changelog/13540.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
```release-note:improvement
2+
core: Vault now supports the PROXY protocol v2. Support for UNKNOWN connections
3+
has also been added to the PROXY protocol v1.
4+
```

command/server/listener_tcp_test.go

Lines changed: 257 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
"testing"
1212
"time"
1313

14+
"github.com/hashicorp/go-sockaddr"
1415
"github.com/hashicorp/vault/internalshared/configutil"
1516
"github.com/mitchellh/cli"
17+
"github.com/pires/go-proxyproto"
1618
)
1719

1820
func TestTCPListener(t *testing.T) {
@@ -28,7 +30,7 @@ func TestTCPListener(t *testing.T) {
2830
return net.Dial("tcp", ln.Addr().String())
2931
}
3032

31-
testListenerImpl(t, ln, connFn, "", 0)
33+
testListenerImpl(t, ln, connFn, "", 0, "127.0.0.1", false)
3234
}
3335

3436
// TestTCPListener_tls tests TLS generally
@@ -85,7 +87,7 @@ func TestTCPListener_tls(t *testing.T) {
8587
}
8688
}
8789

88-
testListenerImpl(t, ln, connFn(true), "foo.example.com", 0)
90+
testListenerImpl(t, ln, connFn(true), "foo.example.com", 0, "127.0.0.1", false)
8991

9092
ln, _, _, err = tcpListenerFactory(&configutil.Listener{
9193
Address: "127.0.0.1:0",
@@ -110,7 +112,7 @@ func TestTCPListener_tls(t *testing.T) {
110112
t.Fatalf("err: %s", err)
111113
}
112114

113-
testListenerImpl(t, ln, connFn(false), "foo.example.com", 0)
115+
testListenerImpl(t, ln, connFn(false), "foo.example.com", 0, "127.0.0.1", false)
114116
}
115117

116118
func TestTCPListener_tls13(t *testing.T) {
@@ -167,7 +169,7 @@ func TestTCPListener_tls13(t *testing.T) {
167169
}
168170
}
169171

170-
testListenerImpl(t, ln, connFn(true), "foo.example.com", tls.VersionTLS13)
172+
testListenerImpl(t, ln, connFn(true), "foo.example.com", tls.VersionTLS13, "127.0.0.1", false)
171173

172174
ln, _, _, err = tcpListenerFactory(&configutil.Listener{
173175
Address: "127.0.0.1:0",
@@ -194,7 +196,7 @@ func TestTCPListener_tls13(t *testing.T) {
194196
t.Fatalf("err: %s", err)
195197
}
196198

197-
testListenerImpl(t, ln, connFn(false), "foo.example.com", tls.VersionTLS13)
199+
testListenerImpl(t, ln, connFn(false), "foo.example.com", tls.VersionTLS13, "127.0.0.1", false)
198200

199201
ln, _, _, err = tcpListenerFactory(&configutil.Listener{
200202
Address: "127.0.0.1:0",
@@ -208,5 +210,254 @@ func TestTCPListener_tls13(t *testing.T) {
208210
t.Fatalf("err: %s", err)
209211
}
210212

211-
testListenerImpl(t, ln, connFn(false), "foo.example.com", tls.VersionTLS12)
213+
testListenerImpl(t, ln, connFn(false), "foo.example.com", tls.VersionTLS12, "127.0.0.1", false)
214+
}
215+
216+
func TestTCPListener_proxyProtocol(t *testing.T) {
217+
for name, tc := range map[string]struct {
218+
Behavior string
219+
Header *proxyproto.Header
220+
AuthorizedAddr string
221+
ExpectedAddr string
222+
ExpectError bool
223+
}{
224+
"none-no-header": {
225+
Behavior: "",
226+
ExpectedAddr: "127.0.0.1",
227+
Header: nil,
228+
},
229+
"none-v1": {
230+
Behavior: "",
231+
ExpectedAddr: "127.0.0.1",
232+
ExpectError: true,
233+
Header: &proxyproto.Header{
234+
Version: 1,
235+
Command: proxyproto.PROXY,
236+
TransportProtocol: proxyproto.TCPv4,
237+
SourceAddr: &net.TCPAddr{
238+
IP: net.ParseIP("10.1.1.1"),
239+
Port: 1000,
240+
},
241+
DestinationAddr: &net.TCPAddr{
242+
IP: net.ParseIP("20.2.2.2"),
243+
Port: 2000,
244+
},
245+
},
246+
},
247+
"none-v2": {
248+
Behavior: "",
249+
ExpectedAddr: "127.0.0.1",
250+
ExpectError: true,
251+
Header: &proxyproto.Header{
252+
Version: 2,
253+
Command: proxyproto.PROXY,
254+
TransportProtocol: proxyproto.TCPv4,
255+
SourceAddr: &net.TCPAddr{
256+
IP: net.ParseIP("10.1.1.1"),
257+
Port: 1000,
258+
},
259+
DestinationAddr: &net.TCPAddr{
260+
IP: net.ParseIP("20.2.2.2"),
261+
Port: 2000,
262+
},
263+
},
264+
},
265+
266+
// use_always makes it possible to send the PROXY header but does not
267+
// require it
268+
"use_always-no-header": {
269+
Behavior: "use_always",
270+
ExpectedAddr: "127.0.0.1",
271+
Header: nil,
272+
},
273+
274+
"use_always-header-v1": {
275+
Behavior: "use_always",
276+
ExpectedAddr: "10.1.1.1",
277+
Header: &proxyproto.Header{
278+
Version: 1,
279+
Command: proxyproto.PROXY,
280+
TransportProtocol: proxyproto.TCPv4,
281+
SourceAddr: &net.TCPAddr{
282+
IP: net.ParseIP("10.1.1.1"),
283+
Port: 1000,
284+
},
285+
DestinationAddr: &net.TCPAddr{
286+
IP: net.ParseIP("20.2.2.2"),
287+
Port: 2000,
288+
},
289+
},
290+
},
291+
"use_always-header-v1-unknown": {
292+
Behavior: "use_always",
293+
ExpectedAddr: "127.0.0.1",
294+
Header: &proxyproto.Header{
295+
Version: 1,
296+
Command: proxyproto.PROXY,
297+
TransportProtocol: proxyproto.UNSPEC,
298+
},
299+
},
300+
"use_always-header-v2": {
301+
Behavior: "use_always",
302+
ExpectedAddr: "10.1.1.1",
303+
Header: &proxyproto.Header{
304+
Version: 2,
305+
Command: proxyproto.PROXY,
306+
TransportProtocol: proxyproto.TCPv4,
307+
SourceAddr: &net.TCPAddr{
308+
IP: net.ParseIP("10.1.1.1"),
309+
Port: 1000,
310+
},
311+
DestinationAddr: &net.TCPAddr{
312+
IP: net.ParseIP("20.2.2.2"),
313+
Port: 2000,
314+
},
315+
},
316+
},
317+
"use_always-header-v2-unknown": {
318+
Behavior: "use_always",
319+
ExpectedAddr: "127.0.0.1",
320+
Header: &proxyproto.Header{
321+
Version: 2,
322+
Command: proxyproto.LOCAL,
323+
TransportProtocol: proxyproto.UNSPEC,
324+
},
325+
},
326+
"allow_authorized-no-header-in": {
327+
Behavior: "allow_authorized",
328+
AuthorizedAddr: "127.0.0.1/32",
329+
ExpectedAddr: "127.0.0.1",
330+
},
331+
"allow_authorized-no-header-not-in": {
332+
Behavior: "allow_authorized",
333+
AuthorizedAddr: "10.0.0.1/32",
334+
ExpectedAddr: "127.0.0.1",
335+
},
336+
"allow_authorized-v1-in": {
337+
Behavior: "allow_authorized",
338+
AuthorizedAddr: "127.0.0.1/32",
339+
ExpectedAddr: "10.1.1.1",
340+
Header: &proxyproto.Header{
341+
Version: 1,
342+
Command: proxyproto.PROXY,
343+
TransportProtocol: proxyproto.TCPv4,
344+
SourceAddr: &net.TCPAddr{
345+
IP: net.ParseIP("10.1.1.1"),
346+
Port: 1000,
347+
},
348+
DestinationAddr: &net.TCPAddr{
349+
IP: net.ParseIP("20.2.2.2"),
350+
Port: 2000,
351+
},
352+
},
353+
},
354+
355+
// allow_authorized still accepts the PROXY header when not in the
356+
// authorized addresses but discards it silently
357+
"allow_authorized-v1-not-in": {
358+
Behavior: "allow_authorized",
359+
AuthorizedAddr: "10.0.0.1/32",
360+
ExpectedAddr: "127.0.0.1",
361+
Header: &proxyproto.Header{
362+
Version: 1,
363+
Command: proxyproto.PROXY,
364+
TransportProtocol: proxyproto.TCPv4,
365+
SourceAddr: &net.TCPAddr{
366+
IP: net.ParseIP("10.1.1.1"),
367+
Port: 1000,
368+
},
369+
DestinationAddr: &net.TCPAddr{
370+
IP: net.ParseIP("20.2.2.2"),
371+
Port: 2000,
372+
},
373+
},
374+
},
375+
376+
"deny_unauthorized-no-header-in": {
377+
Behavior: "deny_unauthorized",
378+
AuthorizedAddr: "127.0.0.1/32",
379+
ExpectedAddr: "127.0.0.1",
380+
},
381+
"deny_unauthorized-no-header-not-in": {
382+
Behavior: "deny_unauthorized",
383+
AuthorizedAddr: "10.0.0.1/32",
384+
ExpectedAddr: "127.0.0.1",
385+
ExpectError: true,
386+
},
387+
"deny_unauthorized-v1-in": {
388+
Behavior: "deny_unauthorized",
389+
AuthorizedAddr: "127.0.0.1/32",
390+
ExpectedAddr: "10.1.1.1",
391+
Header: &proxyproto.Header{
392+
Version: 1,
393+
Command: proxyproto.PROXY,
394+
TransportProtocol: proxyproto.TCPv4,
395+
SourceAddr: &net.TCPAddr{
396+
IP: net.ParseIP("10.1.1.1"),
397+
Port: 1000,
398+
},
399+
DestinationAddr: &net.TCPAddr{
400+
IP: net.ParseIP("20.2.2.2"),
401+
Port: 2000,
402+
},
403+
},
404+
},
405+
"deny_unauthorized-v1-not-in": {
406+
Behavior: "deny_unauthorized",
407+
AuthorizedAddr: "10.0.0.1/32",
408+
ExpectedAddr: "127.0.0.1",
409+
ExpectError: true,
410+
Header: &proxyproto.Header{
411+
Version: 1,
412+
Command: proxyproto.PROXY,
413+
TransportProtocol: proxyproto.TCPv4,
414+
SourceAddr: &net.TCPAddr{
415+
IP: net.ParseIP("10.1.1.1"),
416+
Port: 1000,
417+
},
418+
DestinationAddr: &net.TCPAddr{
419+
IP: net.ParseIP("20.2.2.2"),
420+
Port: 2000,
421+
},
422+
},
423+
},
424+
} {
425+
t.Run(name, func(t *testing.T) {
426+
proxyProtocolAuthorizedAddrs := []*sockaddr.SockAddrMarshaler{}
427+
if tc.AuthorizedAddr != "" {
428+
sockAddr, err := sockaddr.NewSockAddr(tc.AuthorizedAddr)
429+
if err != nil {
430+
t.Fatal(err)
431+
}
432+
proxyProtocolAuthorizedAddrs = append(
433+
proxyProtocolAuthorizedAddrs,
434+
&sockaddr.SockAddrMarshaler{SockAddr: sockAddr},
435+
)
436+
}
437+
438+
ln, _, _, err := tcpListenerFactory(&configutil.Listener{
439+
Address: "127.0.0.1:0",
440+
TLSDisable: true,
441+
ProxyProtocolBehavior: tc.Behavior,
442+
ProxyProtocolAuthorizedAddrs: proxyProtocolAuthorizedAddrs,
443+
}, nil, cli.NewMockUi())
444+
if err != nil {
445+
t.Fatalf("err: %s", err)
446+
}
447+
448+
connFn := func(lnReal net.Listener) (net.Conn, error) {
449+
conn, err := net.Dial("tcp", ln.Addr().String())
450+
if err != nil {
451+
return nil, err
452+
}
453+
454+
if tc.Header != nil {
455+
_, err = tc.Header.WriteTo(conn)
456+
}
457+
return conn, err
458+
}
459+
460+
testListenerImpl(t, ln, connFn, "", 0, tc.ExpectedAddr, tc.ExpectError)
461+
})
462+
}
212463
}

command/server/listener_test.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,29 @@ import (
1010

1111
type testListenerConnFn func(net.Listener) (net.Conn, error)
1212

13-
func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn, certName string, expectedVersion uint16) {
13+
func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn, certName string, expectedVersion uint16, expectedAddr string, expectError bool) {
1414
serverCh := make(chan net.Conn, 1)
1515
go func() {
1616
server, err := ln.Accept()
1717
if err != nil {
18-
t.Errorf("err: %s", err)
18+
if !expectError {
19+
t.Errorf("err: %s", err)
20+
}
21+
close(serverCh)
1922
return
2023
}
2124
if certName != "" {
2225
tlsConn := server.(*tls.Conn)
2326
tlsConn.Handshake()
2427
}
2528
serverCh <- server
29+
addr, _, err := net.SplitHostPort(server.RemoteAddr().String())
30+
if err != nil {
31+
t.Error(err)
32+
}
33+
if addr != expectedAddr {
34+
t.Errorf("expected: %s, got: %s", expectedAddr, addr)
35+
}
2636
}()
2737

2838
client, err := connFn(ln)
@@ -45,6 +55,15 @@ func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn,
4555
}
4656

4757
server := <-serverCh
58+
59+
if server == nil {
60+
if !expectError {
61+
// Something failed already so we abort the test early
62+
t.Fatal("aborting test because the server did not accept the connection")
63+
}
64+
return
65+
}
66+
4867
defer client.Close()
4968
defer server.Close()
5069

@@ -62,8 +81,8 @@ func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn,
6281
client.Close()
6382

6483
<-copyCh
65-
if buf.String() != "foo" {
66-
t.Fatalf("bad: %v", buf.String())
84+
if (buf.String() != "foo" && !expectError) || (buf.String() == "foo" && expectError) {
85+
t.Fatalf("bad: %q, expectError: %t", buf.String(), expectError)
6786
}
6887
}
6988

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ require (
2727
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5
2828
github.com/apple/foundationdb/bindings/go v0.0.0-20190411004307-cd5c9d91fad2
2929
github.com/armon/go-metrics v0.3.10
30-
github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a
3130
github.com/armon/go-radix v1.0.0
3231
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
3332
github.com/aws/aws-sdk-go v1.37.19
@@ -151,6 +150,7 @@ require (
151150
github.com/ory/dockertest v3.3.5+incompatible
152151
github.com/ory/dockertest/v3 v3.8.0
153152
github.com/patrickmn/go-cache v2.1.0+incompatible
153+
github.com/pires/go-proxyproto v0.6.1
154154
github.com/pkg/errors v0.9.1
155155
github.com/posener/complete v1.2.3
156156
github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d

0 commit comments

Comments
 (0)