From 22b835b9f497adfa226f175c1a04709a3e7ef02d Mon Sep 17 00:00:00 2001 From: twdkeule Date: Tue, 12 Sep 2017 03:49:02 +0200 Subject: [PATCH] proxy: Support QUIC for upstream connections (#1782) * Proxy can now use QUIC for upstream connections Add HandshakeTimeout, change h2quic syntax * Add setup and upstream test Test QUIC proxy with actual h2quic instance Use different port fo QUIC test server Add quic host to CI config Added testdata to vendor Revert "Added testdata to vendor" This reverts commit 959512282deed8623168d090e5ca5e5a7933019c. * Use local testdata --- .travis.yml | 4 ++ appveyor.yml | 3 ++ caddyhttp/proxy/proxy_test.go | 58 ++++++++++++++++++++++++++ caddyhttp/proxy/reverseproxy.go | 24 ++++++++++- caddyhttp/proxy/setup_test.go | 8 ++++ caddyhttp/proxy/testdata/fullchain.pem | 56 +++++++++++++++++++++++++ caddyhttp/proxy/testdata/privkey.pem | 28 +++++++++++++ caddyhttp/proxy/upstream.go | 3 +- caddyhttp/proxy/upstream_test.go | 36 ++++++++++++++++ 9 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 caddyhttp/proxy/testdata/fullchain.pem create mode 100644 caddyhttp/proxy/testdata/privkey.pem diff --git a/.travis.yml b/.travis.yml index 203269e2..912c5d05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: go +addons: + hosts: + - quic.clemente.io + go: - 1.9 - tip diff --git a/appveyor.yml b/appveyor.yml index bde9acb4..dfb5e1c4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,8 @@ version: "{build}" +hosts: + quic.clemente.io: 127.0.0.1 + os: Windows Server 2012 R2 clone_folder: c:\gopath\src\github.com\mholt\caddy diff --git a/caddyhttp/proxy/proxy_test.go b/caddyhttp/proxy/proxy_test.go index d7342560..d82dbf38 100644 --- a/caddyhttp/proxy/proxy_test.go +++ b/caddyhttp/proxy/proxy_test.go @@ -14,6 +14,7 @@ import ( "net/http/httptest" "net/url" "os" + "path" "path/filepath" "reflect" "runtime" @@ -23,6 +24,7 @@ import ( "testing" "time" + "github.com/lucas-clemente/quic-go/h2quic" "github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyhttp/httpserver" @@ -1470,3 +1472,59 @@ func TestChunkedWebSocketReverseProxy(t *testing.T) { t.Error(err) } } + +func TestQuic(t *testing.T) { + upstream := "quic.clemente.io:8086" + config := "proxy / quic://" + upstream + content := "Hello, client" + + // make proxy + upstreams, err := NewStaticUpstreams(caddyfile.NewDispenser("Testfile", strings.NewReader(config)), "") + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + } + p := &Proxy{ + Next: httpserver.EmptyNext, // prevents panic in some cases when test fails + Upstreams: upstreams, + } + + // start QUIC server + go func() { + dir, err := os.Getwd() + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + return + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(content)) + w.WriteHeader(200) + }) + err = h2quic.ListenAndServeQUIC( + upstream, + path.Join(dir, "testdata", "fullchain.pem"), + path.Join(dir, "testdata", "privkey.pem"), + handler, + ) + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + return + } + }() + + r := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + _, err = p.ServeHTTP(w, r) + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + return + } + + // check response + if w.Code != 200 { + t.Errorf("Expected response code 200, got: %d", w.Code) + } + responseContent := string(w.Body.Bytes()) + if responseContent != content { + t.Errorf("Expected response body, got: %s", responseContent) + } +} diff --git a/caddyhttp/proxy/reverseproxy.go b/caddyhttp/proxy/reverseproxy.go index 41687cc1..49660749 100644 --- a/caddyhttp/proxy/reverseproxy.go +++ b/caddyhttp/proxy/reverseproxy.go @@ -23,6 +23,8 @@ import ( "golang.org/x/net/http2" + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/h2quic" "github.com/mholt/caddy/caddyhttp/httpserver" ) @@ -33,6 +35,8 @@ var ( } bufferPool = sync.Pool{New: createBuffer} + + defaultCryptoHandshakeTimeout = 10 * time.Second ) func createBuffer() interface{} { @@ -180,11 +184,18 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) * req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery } } + rp := &ReverseProxy{Director: director, FlushInterval: 250 * time.Millisecond} // flushing good for streaming & server-sent events if target.Scheme == "unix" { rp.Transport = &http.Transport{ Dial: socketDial(target.String()), } + } else if target.Scheme == "quic" { + rp.Transport = &h2quic.RoundTripper{ + QuicConfig: &quic.Config{ + HandshakeTimeout: defaultCryptoHandshakeTimeout, + }, + } } else if keepalive != http.DefaultMaxIdleConnsPerHost { // if keepalive is equal to the default, // just use default transport, to avoid creating @@ -192,7 +203,7 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) * transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: defaultDialer.Dial, - TLSHandshakeTimeout: 10 * time.Second, + TLSHandshakeTimeout: defaultCryptoHandshakeTimeout, ExpectContinueTimeout: 1 * time.Second, } if keepalive == 0 { @@ -216,7 +227,7 @@ func (rp *ReverseProxy) UseInsecureTransport() { transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: defaultDialer.Dial, - TLSHandshakeTimeout: 10 * time.Second, + TLSHandshakeTimeout: defaultCryptoHandshakeTimeout, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } if httpserver.HTTP2 { @@ -231,6 +242,11 @@ func (rp *ReverseProxy) UseInsecureTransport() { // No http2.ConfigureTransport() here. // For now this is only added in places where // an http.Transport is actually created. + } else if transport, ok := rp.Transport.(*h2quic.RoundTripper); ok { + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + transport.TLSClientConfig.InsecureSkipVerify = true } } @@ -246,6 +262,10 @@ func (rp *ReverseProxy) ServeHTTP(rw http.ResponseWriter, outreq *http.Request, rp.Director(outreq) + if outreq.URL.Scheme == "quic" { + outreq.URL.Scheme = "https" // Change scheme back to https for QUIC RoundTripper + } + res, err := transport.RoundTrip(outreq) if err != nil { return err diff --git a/caddyhttp/proxy/setup_test.go b/caddyhttp/proxy/setup_test.go index 02809058..0e455ae3 100644 --- a/caddyhttp/proxy/setup_test.go +++ b/caddyhttp/proxy/setup_test.go @@ -147,6 +147,14 @@ func TestSetup(t *testing.T) { "http://localhost:1984": {}, }, }, + // test #14 test QUIC + { + "proxy / quic://localhost:443", + false, + map[string]struct{}{ + "quic://localhost:443": {}, + }, + }, } { c := caddy.NewTestController("http", test.input) err := setup(c) diff --git a/caddyhttp/proxy/testdata/fullchain.pem b/caddyhttp/proxy/testdata/fullchain.pem new file mode 100644 index 00000000..6a5aef00 --- /dev/null +++ b/caddyhttp/proxy/testdata/fullchain.pem @@ -0,0 +1,56 @@ +-----BEGIN CERTIFICATE----- +MIIFAzCCA+ugAwIBAgISA7e2G9wJth5EaqaD5X0RiagYMA0GCSqGSIb3DQEBCwUA +MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD +ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xNzA3MDMxODU3MDBaFw0x +NzEwMDExODU3MDBaMBsxGTAXBgNVBAMTEHF1aWMuY2xlbWVudGUuaW8wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7UjonSCiB0tyHsenbXZw/QF028EmH +tvdyMTOlz2DVLqi0K6brqVIh3KSl2gPlsizLHmkoTLVINuGCnDXc4jXu6yCVHrPr +KOf+ip8SUcqQEmLXmHw5Y+L4/6ZKUE5mFpfqmlMCEb1t86J7FI9z+QA9LGdbziYv +qQBW8GytX16OJ4h/S1fiPCQ5GfWtkoVgYzgz8Vn4o51lLG2YXAl451BR8+XhGlYS +OjS6x7RA0F5wqCeGgro7wKbFuyfxrkWkVzn5hNdEkBAABiub6obNBMZ6v+u84bQk +1rH0oZB5rn3uEPycLmrQF2cYR5b+2F+BymKC0ElkFi3iWQoO98SZZQinAgMBAAGj +ggIQMIICDDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNjHumfJ0g905MebRnAvNfQh +3AvEMB8GA1UdIwQYMBaAFKhKamMEfd265tE5t6ZFZe/zqOyhMG8GCCsGAQUFBwEB +BGMwYTAuBggrBgEFBQcwAYYiaHR0cDovL29jc3AuaW50LXgzLmxldHNlbmNyeXB0 +Lm9yZzAvBggrBgEFBQcwAoYjaHR0cDovL2NlcnQuaW50LXgzLmxldHNlbmNyeXB0 +Lm9yZy8wGwYDVR0RBBQwEoIQcXVpYy5jbGVtZW50ZS5pbzCB/gYDVR0gBIH2MIHz +MAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6 +Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2Vy +dGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0 +aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUg +UG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRv +cnkvMA0GCSqGSIb3DQEBCwUAA4IBAQAqs3Mrr/Erqp1rOFkLwKbStWZniCvqhl58 +VnScP2CjiBsaLJUuBlWqC215FtX5CrdkIwYrMMkkOZHZI4mPxN64UVqMY5UJRonL +GvkeHC5QYsCV09bBHjCei6JDItNH2PCec9+mV9EIQiVzd8xliE3t0eTbjNsa9zf1 +Qwp64THbiyTIXuh4xgFTxU2u58+RkIRbKGRM1X4jgIv8xjNV4P1c0jUVqaEFkCjR +A03becsSv3wqWvPCNQRdVRdoMMghHenDEAGD621McnaXDoNz8pgn/ss1vzrO36gX +WZ7CmbgIFdYeMgqQop/252bN2wrNjnxAjLAHo/X1MPEabjoL1C0g +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow +SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT +GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF +q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 +SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 +Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA +a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj +/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG +CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv +bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k +c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw +VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC +ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz +MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu +Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF +AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo +uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ +wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu +X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG +PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 +KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== +-----END CERTIFICATE----- diff --git a/caddyhttp/proxy/testdata/privkey.pem b/caddyhttp/proxy/testdata/privkey.pem new file mode 100644 index 00000000..077874c4 --- /dev/null +++ b/caddyhttp/proxy/testdata/privkey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7UjonSCiB0tyH +senbXZw/QF028EmHtvdyMTOlz2DVLqi0K6brqVIh3KSl2gPlsizLHmkoTLVINuGC +nDXc4jXu6yCVHrPrKOf+ip8SUcqQEmLXmHw5Y+L4/6ZKUE5mFpfqmlMCEb1t86J7 +FI9z+QA9LGdbziYvqQBW8GytX16OJ4h/S1fiPCQ5GfWtkoVgYzgz8Vn4o51lLG2Y +XAl451BR8+XhGlYSOjS6x7RA0F5wqCeGgro7wKbFuyfxrkWkVzn5hNdEkBAABiub +6obNBMZ6v+u84bQk1rH0oZB5rn3uEPycLmrQF2cYR5b+2F+BymKC0ElkFi3iWQoO +98SZZQinAgMBAAECggEAbO8EopNz+wuE8+Si+s8VbjMgAjL6j9H3VJEIWASha1gX +A6/fAm0VNlv54/lFCu7y3axxut3hDn3b5viw2iMy+h4CdLXGK5s+TuiOWTj3c5E9 +qeMjWryb4fHJ4q2Q6g15ixTz8OAgKTDl7G2ofujvGqQX92uLCWxepjBrAufTNRcJ +OZ3ngqHlKsRXX3nXkAMYrypK7ALF2kuavAGNrDQvPWUZKp3vuvd3Hx/stw0s3Th1 +XrHZnaAMZlxZg32IiVxs3vR2sACJ0YyOBpERBjjBsIaeyNXfZVrEmNzvo6iVhdhN +ZNxrKSnPEfTdFk5pldFbTzNpvCvjbFAlE0aHXNRJAQKBgQDpAzWGkOTE+wmcWJNk +oRi4ZJHhK/kckvNg1OZMXAqqZJOPxvwatEFgQ1GZo8rhSzdf64kB9b9I3OjEhd8r +M90pt57BqRSq5rbytZBdR2BcbNnKkYF204AS2pkEVvkOVnWz5zSVhd8a0gMx3EdE +LKN0r+DLKune8cnAS0BDvBjf3QKBgQDNzRJe9pI29mxUyuLQuKngaa8KmPy1EpbW ++d21ET4MjZbH6uPOAe1Q+7aCEA7rjvFoOqGk0w1WIN0i8EaIOuwM2W0jw4VS7AVI +rWXTYy9uSnUuLWL6gHNbehqLs6JaADEvytWdXdqiR/XWxCDn7qg2CrjxwmDB/OUm +RopmnlkEUwKBgQCreZ4ZUmXYhDmVYiXN5zPO9svYHkkr+wS6HNMCHLYIoQ1qwG/k +owR9d+0EGOKDm5u7rhTcaWIEl/WAMliCbZ9zRNrC/8/i2PiHcpAz5QQH4F8CUMQq +kwjsVwxGgk60e3IRG7O52ZPPJAAP4GBdzk/X3lqaiREk7WCgb4BymGjhzQKBgEMF +mQkCJeXuZKNMm4c7zF8AK/g4kHvrvOHv56sTHXD7H3Kl5WBusjmgb/R1hFZka+v0 +xDWoYfx9oWbCd0XgYoVgvbFa+G1j3eioR7QK5iR17SmHsGdCM89DuadrbeD/lQUq +elzQduZIpyA1KT4/M9q9rTNWiSpD0OChMmtvADBvAoGAAXF3cARv5w0fSZGSRCOw +U3LdFNIhBgVdROj2C4ym+uJFErKTkB5kghdUER7UsFH8fVn3JLAb35cQRYGrysYz +XF5eK0akNhkO9GLNrK0GbSHKZm9vQxixm5W05aVoUofRHqkkKL1ceC2rhwzp3Q5P +1jLabOA4K0DkhNga0YPKJLQ= +-----END PRIVATE KEY----- diff --git a/caddyhttp/proxy/upstream.go b/caddyhttp/proxy/upstream.go index e7cc392b..4457dd0e 100644 --- a/caddyhttp/proxy/upstream.go +++ b/caddyhttp/proxy/upstream.go @@ -150,7 +150,8 @@ func (u *staticUpstream) From() string { func (u *staticUpstream) NewHost(host string) (*UpstreamHost, error) { if !strings.HasPrefix(host, "http") && - !strings.HasPrefix(host, "unix:") { + !strings.HasPrefix(host, "unix:") && + !strings.HasPrefix(host, "quic:") { host = "http://" + host } uh := &UpstreamHost{ diff --git a/caddyhttp/proxy/upstream_test.go b/caddyhttp/proxy/upstream_test.go index 8d1ef719..af31ef77 100644 --- a/caddyhttp/proxy/upstream_test.go +++ b/caddyhttp/proxy/upstream_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/lucas-clemente/quic-go/h2quic" "github.com/mholt/caddy/caddyfile" ) @@ -501,3 +502,38 @@ func TestHealthCheckContentString(t *testing.T) { } } } + +func TestQuicHost(t *testing.T) { + // tests for QUIC proxy + tests := []struct { + config string + flag bool + }{ + // Test #1: without flag + {"proxy / quic://localhost:8080", false}, + + // Test #2: with flag + {"proxy / quic://localhost:8080 {\n insecure_skip_verify \n}", true}, + } + + for _, test := range tests { + upstreams, err := NewStaticUpstreams(caddyfile.NewDispenser("Testfile", strings.NewReader(test.config)), "") + if err != nil { + t.Errorf("Expected no error. Got: %s", err.Error()) + } + for _, upstream := range upstreams { + staticUpstream, ok := upstream.(*staticUpstream) + if !ok { + t.Errorf("Type mismatch: %#v", upstream) + continue + } + for _, host := range staticUpstream.Hosts { + _, ok := host.ReverseProxy.Transport.(*h2quic.RoundTripper) + if !ok { + t.Errorf("Type mismatch: %#v", host.ReverseProxy.Transport) + continue + } + } + } + } +}