0
Fork 0
mirror of https://github.com/caddyserver/caddy.git synced 2024-12-23 22:27:38 -05:00

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
This commit is contained in:
twdkeule 2017-09-12 03:49:02 +02:00 committed by Matt Holt
parent 46ae4a6652
commit 22b835b9f4
9 changed files with 217 additions and 3 deletions

View file

@ -1,5 +1,9 @@
language: go language: go
addons:
hosts:
- quic.clemente.io
go: go:
- 1.9 - 1.9
- tip - tip

View file

@ -1,5 +1,8 @@
version: "{build}" version: "{build}"
hosts:
quic.clemente.io: 127.0.0.1
os: Windows Server 2012 R2 os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\mholt\caddy clone_folder: c:\gopath\src\github.com\mholt\caddy

View file

@ -14,6 +14,7 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime" "runtime"
@ -23,6 +24,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/lucas-clemente/quic-go/h2quic"
"github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
@ -1470,3 +1472,59 @@ func TestChunkedWebSocketReverseProxy(t *testing.T) {
t.Error(err) 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)
}
}

View file

@ -23,6 +23,8 @@ import (
"golang.org/x/net/http2" "golang.org/x/net/http2"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/h2quic"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
@ -33,6 +35,8 @@ var (
} }
bufferPool = sync.Pool{New: createBuffer} bufferPool = sync.Pool{New: createBuffer}
defaultCryptoHandshakeTimeout = 10 * time.Second
) )
func createBuffer() interface{} { func createBuffer() interface{} {
@ -180,11 +184,18 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) *
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
} }
} }
rp := &ReverseProxy{Director: director, FlushInterval: 250 * time.Millisecond} // flushing good for streaming & server-sent events rp := &ReverseProxy{Director: director, FlushInterval: 250 * time.Millisecond} // flushing good for streaming & server-sent events
if target.Scheme == "unix" { if target.Scheme == "unix" {
rp.Transport = &http.Transport{ rp.Transport = &http.Transport{
Dial: socketDial(target.String()), Dial: socketDial(target.String()),
} }
} else if target.Scheme == "quic" {
rp.Transport = &h2quic.RoundTripper{
QuicConfig: &quic.Config{
HandshakeTimeout: defaultCryptoHandshakeTimeout,
},
}
} else if keepalive != http.DefaultMaxIdleConnsPerHost { } else if keepalive != http.DefaultMaxIdleConnsPerHost {
// if keepalive is equal to the default, // if keepalive is equal to the default,
// just use default transport, to avoid creating // just use default transport, to avoid creating
@ -192,7 +203,7 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) *
transport := &http.Transport{ transport := &http.Transport{
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
Dial: defaultDialer.Dial, Dial: defaultDialer.Dial,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: defaultCryptoHandshakeTimeout,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
} }
if keepalive == 0 { if keepalive == 0 {
@ -216,7 +227,7 @@ func (rp *ReverseProxy) UseInsecureTransport() {
transport := &http.Transport{ transport := &http.Transport{
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
Dial: defaultDialer.Dial, Dial: defaultDialer.Dial,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: defaultCryptoHandshakeTimeout,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
} }
if httpserver.HTTP2 { if httpserver.HTTP2 {
@ -231,6 +242,11 @@ func (rp *ReverseProxy) UseInsecureTransport() {
// No http2.ConfigureTransport() here. // No http2.ConfigureTransport() here.
// For now this is only added in places where // For now this is only added in places where
// an http.Transport is actually created. // 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) 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) res, err := transport.RoundTrip(outreq)
if err != nil { if err != nil {
return err return err

View file

@ -147,6 +147,14 @@ func TestSetup(t *testing.T) {
"http://localhost:1984": {}, "http://localhost:1984": {},
}, },
}, },
// test #14 test QUIC
{
"proxy / quic://localhost:443",
false,
map[string]struct{}{
"quic://localhost:443": {},
},
},
} { } {
c := caddy.NewTestController("http", test.input) c := caddy.NewTestController("http", test.input)
err := setup(c) err := setup(c)

56
caddyhttp/proxy/testdata/fullchain.pem vendored Normal file
View file

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

28
caddyhttp/proxy/testdata/privkey.pem vendored Normal file
View file

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

View file

@ -150,7 +150,8 @@ func (u *staticUpstream) From() string {
func (u *staticUpstream) NewHost(host string) (*UpstreamHost, error) { func (u *staticUpstream) NewHost(host string) (*UpstreamHost, error) {
if !strings.HasPrefix(host, "http") && if !strings.HasPrefix(host, "http") &&
!strings.HasPrefix(host, "unix:") { !strings.HasPrefix(host, "unix:") &&
!strings.HasPrefix(host, "quic:") {
host = "http://" + host host = "http://" + host
} }
uh := &UpstreamHost{ uh := &UpstreamHost{

View file

@ -10,6 +10,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/lucas-clemente/quic-go/h2quic"
"github.com/mholt/caddy/caddyfile" "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
}
}
}
}
}