diff --git a/changelog/13540.txt b/changelog/13540.txt new file mode 100644 index 000000000..05fd83d4c --- /dev/null +++ b/changelog/13540.txt @@ -0,0 +1,4 @@ +```release-note:improvement +core: Vault now supports the PROXY protocol v2. Support for UNKNOWN connections +has also been added to the PROXY protocol v1. +``` diff --git a/command/server/listener_tcp_test.go b/command/server/listener_tcp_test.go index 791bdaffc..5ebf61114 100644 --- a/command/server/listener_tcp_test.go +++ b/command/server/listener_tcp_test.go @@ -11,8 +11,10 @@ import ( "testing" "time" + "github.com/hashicorp/go-sockaddr" "github.com/hashicorp/vault/internalshared/configutil" "github.com/mitchellh/cli" + "github.com/pires/go-proxyproto" ) func TestTCPListener(t *testing.T) { @@ -28,7 +30,7 @@ func TestTCPListener(t *testing.T) { return net.Dial("tcp", ln.Addr().String()) } - testListenerImpl(t, ln, connFn, "", 0) + testListenerImpl(t, ln, connFn, "", 0, "127.0.0.1", false) } // TestTCPListener_tls tests TLS generally @@ -85,7 +87,7 @@ func TestTCPListener_tls(t *testing.T) { } } - testListenerImpl(t, ln, connFn(true), "foo.example.com", 0) + testListenerImpl(t, ln, connFn(true), "foo.example.com", 0, "127.0.0.1", false) ln, _, _, err = tcpListenerFactory(&configutil.Listener{ Address: "127.0.0.1:0", @@ -110,7 +112,7 @@ func TestTCPListener_tls(t *testing.T) { t.Fatalf("err: %s", err) } - testListenerImpl(t, ln, connFn(false), "foo.example.com", 0) + testListenerImpl(t, ln, connFn(false), "foo.example.com", 0, "127.0.0.1", false) } func TestTCPListener_tls13(t *testing.T) { @@ -167,7 +169,7 @@ func TestTCPListener_tls13(t *testing.T) { } } - testListenerImpl(t, ln, connFn(true), "foo.example.com", tls.VersionTLS13) + testListenerImpl(t, ln, connFn(true), "foo.example.com", tls.VersionTLS13, "127.0.0.1", false) ln, _, _, err = tcpListenerFactory(&configutil.Listener{ Address: "127.0.0.1:0", @@ -194,7 +196,7 @@ func TestTCPListener_tls13(t *testing.T) { t.Fatalf("err: %s", err) } - testListenerImpl(t, ln, connFn(false), "foo.example.com", tls.VersionTLS13) + testListenerImpl(t, ln, connFn(false), "foo.example.com", tls.VersionTLS13, "127.0.0.1", false) ln, _, _, err = tcpListenerFactory(&configutil.Listener{ Address: "127.0.0.1:0", @@ -208,5 +210,254 @@ func TestTCPListener_tls13(t *testing.T) { t.Fatalf("err: %s", err) } - testListenerImpl(t, ln, connFn(false), "foo.example.com", tls.VersionTLS12) + testListenerImpl(t, ln, connFn(false), "foo.example.com", tls.VersionTLS12, "127.0.0.1", false) +} + +func TestTCPListener_proxyProtocol(t *testing.T) { + for name, tc := range map[string]struct { + Behavior string + Header *proxyproto.Header + AuthorizedAddr string + ExpectedAddr string + ExpectError bool + }{ + "none-no-header": { + Behavior: "", + ExpectedAddr: "127.0.0.1", + Header: nil, + }, + "none-v1": { + Behavior: "", + ExpectedAddr: "127.0.0.1", + ExpectError: true, + Header: &proxyproto.Header{ + Version: 1, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: &net.TCPAddr{ + IP: net.ParseIP("10.1.1.1"), + Port: 1000, + }, + DestinationAddr: &net.TCPAddr{ + IP: net.ParseIP("20.2.2.2"), + Port: 2000, + }, + }, + }, + "none-v2": { + Behavior: "", + ExpectedAddr: "127.0.0.1", + ExpectError: true, + Header: &proxyproto.Header{ + Version: 2, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: &net.TCPAddr{ + IP: net.ParseIP("10.1.1.1"), + Port: 1000, + }, + DestinationAddr: &net.TCPAddr{ + IP: net.ParseIP("20.2.2.2"), + Port: 2000, + }, + }, + }, + + // use_always makes it possible to send the PROXY header but does not + // require it + "use_always-no-header": { + Behavior: "use_always", + ExpectedAddr: "127.0.0.1", + Header: nil, + }, + + "use_always-header-v1": { + Behavior: "use_always", + ExpectedAddr: "10.1.1.1", + Header: &proxyproto.Header{ + Version: 1, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: &net.TCPAddr{ + IP: net.ParseIP("10.1.1.1"), + Port: 1000, + }, + DestinationAddr: &net.TCPAddr{ + IP: net.ParseIP("20.2.2.2"), + Port: 2000, + }, + }, + }, + "use_always-header-v1-unknown": { + Behavior: "use_always", + ExpectedAddr: "127.0.0.1", + Header: &proxyproto.Header{ + Version: 1, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.UNSPEC, + }, + }, + "use_always-header-v2": { + Behavior: "use_always", + ExpectedAddr: "10.1.1.1", + Header: &proxyproto.Header{ + Version: 2, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: &net.TCPAddr{ + IP: net.ParseIP("10.1.1.1"), + Port: 1000, + }, + DestinationAddr: &net.TCPAddr{ + IP: net.ParseIP("20.2.2.2"), + Port: 2000, + }, + }, + }, + "use_always-header-v2-unknown": { + Behavior: "use_always", + ExpectedAddr: "127.0.0.1", + Header: &proxyproto.Header{ + Version: 2, + Command: proxyproto.LOCAL, + TransportProtocol: proxyproto.UNSPEC, + }, + }, + "allow_authorized-no-header-in": { + Behavior: "allow_authorized", + AuthorizedAddr: "127.0.0.1/32", + ExpectedAddr: "127.0.0.1", + }, + "allow_authorized-no-header-not-in": { + Behavior: "allow_authorized", + AuthorizedAddr: "10.0.0.1/32", + ExpectedAddr: "127.0.0.1", + }, + "allow_authorized-v1-in": { + Behavior: "allow_authorized", + AuthorizedAddr: "127.0.0.1/32", + ExpectedAddr: "10.1.1.1", + Header: &proxyproto.Header{ + Version: 1, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: &net.TCPAddr{ + IP: net.ParseIP("10.1.1.1"), + Port: 1000, + }, + DestinationAddr: &net.TCPAddr{ + IP: net.ParseIP("20.2.2.2"), + Port: 2000, + }, + }, + }, + + // allow_authorized still accepts the PROXY header when not in the + // authorized addresses but discards it silently + "allow_authorized-v1-not-in": { + Behavior: "allow_authorized", + AuthorizedAddr: "10.0.0.1/32", + ExpectedAddr: "127.0.0.1", + Header: &proxyproto.Header{ + Version: 1, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: &net.TCPAddr{ + IP: net.ParseIP("10.1.1.1"), + Port: 1000, + }, + DestinationAddr: &net.TCPAddr{ + IP: net.ParseIP("20.2.2.2"), + Port: 2000, + }, + }, + }, + + "deny_unauthorized-no-header-in": { + Behavior: "deny_unauthorized", + AuthorizedAddr: "127.0.0.1/32", + ExpectedAddr: "127.0.0.1", + }, + "deny_unauthorized-no-header-not-in": { + Behavior: "deny_unauthorized", + AuthorizedAddr: "10.0.0.1/32", + ExpectedAddr: "127.0.0.1", + ExpectError: true, + }, + "deny_unauthorized-v1-in": { + Behavior: "deny_unauthorized", + AuthorizedAddr: "127.0.0.1/32", + ExpectedAddr: "10.1.1.1", + Header: &proxyproto.Header{ + Version: 1, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: &net.TCPAddr{ + IP: net.ParseIP("10.1.1.1"), + Port: 1000, + }, + DestinationAddr: &net.TCPAddr{ + IP: net.ParseIP("20.2.2.2"), + Port: 2000, + }, + }, + }, + "deny_unauthorized-v1-not-in": { + Behavior: "deny_unauthorized", + AuthorizedAddr: "10.0.0.1/32", + ExpectedAddr: "127.0.0.1", + ExpectError: true, + Header: &proxyproto.Header{ + Version: 1, + Command: proxyproto.PROXY, + TransportProtocol: proxyproto.TCPv4, + SourceAddr: &net.TCPAddr{ + IP: net.ParseIP("10.1.1.1"), + Port: 1000, + }, + DestinationAddr: &net.TCPAddr{ + IP: net.ParseIP("20.2.2.2"), + Port: 2000, + }, + }, + }, + } { + t.Run(name, func(t *testing.T) { + proxyProtocolAuthorizedAddrs := []*sockaddr.SockAddrMarshaler{} + if tc.AuthorizedAddr != "" { + sockAddr, err := sockaddr.NewSockAddr(tc.AuthorizedAddr) + if err != nil { + t.Fatal(err) + } + proxyProtocolAuthorizedAddrs = append( + proxyProtocolAuthorizedAddrs, + &sockaddr.SockAddrMarshaler{SockAddr: sockAddr}, + ) + } + + ln, _, _, err := tcpListenerFactory(&configutil.Listener{ + Address: "127.0.0.1:0", + TLSDisable: true, + ProxyProtocolBehavior: tc.Behavior, + ProxyProtocolAuthorizedAddrs: proxyProtocolAuthorizedAddrs, + }, nil, cli.NewMockUi()) + if err != nil { + t.Fatalf("err: %s", err) + } + + connFn := func(lnReal net.Listener) (net.Conn, error) { + conn, err := net.Dial("tcp", ln.Addr().String()) + if err != nil { + return nil, err + } + + if tc.Header != nil { + _, err = tc.Header.WriteTo(conn) + } + return conn, err + } + + testListenerImpl(t, ln, connFn, "", 0, tc.ExpectedAddr, tc.ExpectError) + }) + } } diff --git a/command/server/listener_test.go b/command/server/listener_test.go index 6671da9f8..e36ced7e7 100644 --- a/command/server/listener_test.go +++ b/command/server/listener_test.go @@ -10,12 +10,15 @@ import ( type testListenerConnFn func(net.Listener) (net.Conn, error) -func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn, certName string, expectedVersion uint16) { +func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn, certName string, expectedVersion uint16, expectedAddr string, expectError bool) { serverCh := make(chan net.Conn, 1) go func() { server, err := ln.Accept() if err != nil { - t.Errorf("err: %s", err) + if !expectError { + t.Errorf("err: %s", err) + } + close(serverCh) return } if certName != "" { @@ -23,6 +26,13 @@ func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn, tlsConn.Handshake() } serverCh <- server + addr, _, err := net.SplitHostPort(server.RemoteAddr().String()) + if err != nil { + t.Error(err) + } + if addr != expectedAddr { + t.Errorf("expected: %s, got: %s", expectedAddr, addr) + } }() client, err := connFn(ln) @@ -45,6 +55,15 @@ func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn, } server := <-serverCh + + if server == nil { + if !expectError { + // Something failed already so we abort the test early + t.Fatal("aborting test because the server did not accept the connection") + } + return + } + defer client.Close() defer server.Close() @@ -62,8 +81,8 @@ func testListenerImpl(t *testing.T, ln net.Listener, connFn testListenerConnFn, client.Close() <-copyCh - if buf.String() != "foo" { - t.Fatalf("bad: %v", buf.String()) + if (buf.String() != "foo" && !expectError) || (buf.String() == "foo" && expectError) { + t.Fatalf("bad: %q, expectError: %t", buf.String(), expectError) } } diff --git a/go.mod b/go.mod index 85ddbf182..9b7549a75 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,6 @@ require ( github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5 github.com/apple/foundationdb/bindings/go v0.0.0-20190411004307-cd5c9d91fad2 github.com/armon/go-metrics v0.3.10 - github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a github.com/armon/go-radix v1.0.0 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a github.com/aws/aws-sdk-go v1.37.19 @@ -151,6 +150,7 @@ require ( github.com/ory/dockertest v3.3.5+incompatible github.com/ory/dockertest/v3 v3.8.0 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/pires/go-proxyproto v0.6.1 github.com/pkg/errors v0.9.1 github.com/posener/complete v1.2.3 github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d diff --git a/go.sum b/go.sum index 375c23925..f02a646f8 100644 --- a/go.sum +++ b/go.sum @@ -205,8 +205,6 @@ github.com/armon/go-metrics v0.3.4/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4 github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a h1:AP/vsCIvJZ129pdm9Ek7bH7yutN3hByqsMoNrWAxRQc= -github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -1350,6 +1348,8 @@ github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9F github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pires/go-proxyproto v0.6.1 h1:EBupykFmo22SDjv4fQVQd2J9NOoLPmyZA/15ldOGkPw= +github.com/pires/go-proxyproto v0.6.1/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= diff --git a/helper/proxyutil/proxyutil.go b/helper/proxyutil/proxyutil.go index a25a6234f..e679e2cc5 100644 --- a/helper/proxyutil/proxyutil.go +++ b/helper/proxyutil/proxyutil.go @@ -1,14 +1,15 @@ package proxyutil import ( + "errors" "fmt" "net" "sync" "time" - proxyproto "github.com/armon/go-proxyproto" "github.com/hashicorp/go-secure-stdlib/parseutil" sockaddr "github.com/hashicorp/go-sockaddr" + proxyproto "github.com/pires/go-proxyproto" ) // ProxyProtoConfig contains configuration for the PROXY protocol @@ -42,33 +43,33 @@ func WrapInProxyProto(listener net.Listener, config *ProxyProtoConfig) (net.List case "use_always": newLn = &proxyproto.Listener{ Listener: listener, - ProxyHeaderTimeout: 10 * time.Second, + ReadHeaderTimeout: 10 * time.Second, } case "allow_authorized", "deny_unauthorized": newLn = &proxyproto.Listener{ Listener: listener, - ProxyHeaderTimeout: 10 * time.Second, - SourceCheck: func(addr net.Addr) (bool, error) { + ReadHeaderTimeout: 10 * time.Second, + Policy: func(addr net.Addr) (proxyproto.Policy, error) { config.RLock() defer config.RUnlock() sa, err := sockaddr.NewSockAddr(addr.String()) if err != nil { - return false, fmt.Errorf("error parsing remote address: %w", err) + return proxyproto.REJECT, fmt.Errorf("error parsing remote address: %w", err) } for _, authorizedAddr := range config.AuthorizedAddrs { if authorizedAddr.Contains(sa) { - return true, nil + return proxyproto.USE, nil } } if config.Behavior == "allow_authorized" { - return false, nil + return proxyproto.IGNORE, nil } - return false, proxyproto.ErrInvalidUpstream + return proxyproto.REJECT, errors.New(`upstream connection not trusted proxy_protocol_behavior is "deny_unauthorized"`) }, } default: