2021-08-03 09:04:02 +00:00
|
|
|
package connection
|
|
|
|
|
|
|
|
import (
|
2021-08-03 07:09:56 +00:00
|
|
|
"bytes"
|
2021-08-03 09:04:02 +00:00
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
2021-08-03 07:09:56 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2021-08-03 09:04:02 +00:00
|
|
|
"net"
|
2021-08-03 07:09:56 +00:00
|
|
|
"net/http"
|
2021-09-27 13:12:11 +00:00
|
|
|
"net/url"
|
2021-08-03 09:04:02 +00:00
|
|
|
"os"
|
|
|
|
"sync"
|
|
|
|
"testing"
|
2021-12-14 22:52:47 +00:00
|
|
|
"time"
|
2021-08-03 09:04:02 +00:00
|
|
|
|
2021-08-03 07:09:56 +00:00
|
|
|
"github.com/gobwas/ws/wsutil"
|
2021-12-14 22:52:47 +00:00
|
|
|
"github.com/google/uuid"
|
2021-08-03 09:04:02 +00:00
|
|
|
"github.com/lucas-clemente/quic-go"
|
2021-08-03 07:09:56 +00:00
|
|
|
"github.com/pkg/errors"
|
2021-08-03 09:04:02 +00:00
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2021-12-14 22:52:47 +00:00
|
|
|
"github.com/cloudflare/cloudflared/datagramsession"
|
2021-08-03 09:04:02 +00:00
|
|
|
quicpogs "github.com/cloudflare/cloudflared/quic"
|
2021-09-21 06:11:36 +00:00
|
|
|
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
2021-08-03 09:04:02 +00:00
|
|
|
)
|
|
|
|
|
2021-12-14 22:52:47 +00:00
|
|
|
var (
|
TUN-5621: Correctly manage QUIC stream closing
Until this PR, we were naively closing the quic.Stream whenever
the callstack for handling the request (HTTP or TCP) finished.
However, our proxy handler may still be reading or writing from
the quic.Stream at that point, because we return the callstack if
either side finishes, but not necessarily both.
This is a problem for quic-go library because quic.Stream#Close
cannot be called concurrently with quic.Stream#Write
Furthermore, we also noticed that quic.Stream#Close does nothing
to do receiving stream (since, underneath, quic.Stream has 2 streams,
1 for each direction), thus leaking memory, as explained in:
https://github.com/lucas-clemente/quic-go/issues/3322
This PR addresses both problems by wrapping the quic.Stream that
is passed down to the proxying logic and handle all these concerns.
2022-01-27 22:37:45 +00:00
|
|
|
testTLSServerConfig = quicpogs.GenerateTLSConfig()
|
2021-12-14 22:52:47 +00:00
|
|
|
testQUICConfig = &quic.Config{
|
2021-11-14 11:18:05 +00:00
|
|
|
KeepAlive: true,
|
|
|
|
EnableDatagrams: true,
|
2021-08-03 09:04:02 +00:00
|
|
|
}
|
2021-12-14 22:52:47 +00:00
|
|
|
)
|
2021-08-03 09:04:02 +00:00
|
|
|
|
2021-12-14 22:52:47 +00:00
|
|
|
// TestQUICServer tests if a quic server accepts and responds to a quic client with the acceptance protocol.
|
|
|
|
// It also serves as a demonstration for communication with the QUIC connection started by a cloudflared.
|
|
|
|
func TestQUICServer(t *testing.T) {
|
2021-08-03 07:09:56 +00:00
|
|
|
// Start a UDP Listener for QUIC.
|
2021-08-03 09:04:02 +00:00
|
|
|
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
|
|
|
require.NoError(t, err)
|
|
|
|
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer udpListener.Close()
|
2021-08-03 07:09:56 +00:00
|
|
|
|
|
|
|
// This is simply a sample websocket frame message.
|
|
|
|
wsBuf := &bytes.Buffer{}
|
2021-10-19 19:01:17 +00:00
|
|
|
wsutil.WriteClientBinary(wsBuf, []byte("Hello"))
|
2021-08-03 07:09:56 +00:00
|
|
|
|
2021-08-03 09:04:02 +00:00
|
|
|
var tests = []struct {
|
2021-08-03 07:09:56 +00:00
|
|
|
desc string
|
|
|
|
dest string
|
|
|
|
connectionType quicpogs.ConnectionType
|
|
|
|
metadata []quicpogs.Metadata
|
|
|
|
message []byte
|
|
|
|
expectedResponse []byte
|
2021-08-03 09:04:02 +00:00
|
|
|
}{
|
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
desc: "test http proxy",
|
|
|
|
dest: "/ok",
|
|
|
|
connectionType: quicpogs.ConnectionTypeHTTP,
|
|
|
|
metadata: []quicpogs.Metadata{
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpHeader:Cf-Ray",
|
|
|
|
Val: "123123123",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpHost",
|
|
|
|
Val: "cf.host",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpMethod",
|
|
|
|
Val: "GET",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
expectedResponse: []byte("OK"),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "test http body request streaming",
|
TUN-5621: Correctly manage QUIC stream closing
Until this PR, we were naively closing the quic.Stream whenever
the callstack for handling the request (HTTP or TCP) finished.
However, our proxy handler may still be reading or writing from
the quic.Stream at that point, because we return the callstack if
either side finishes, but not necessarily both.
This is a problem for quic-go library because quic.Stream#Close
cannot be called concurrently with quic.Stream#Write
Furthermore, we also noticed that quic.Stream#Close does nothing
to do receiving stream (since, underneath, quic.Stream has 2 streams,
1 for each direction), thus leaking memory, as explained in:
https://github.com/lucas-clemente/quic-go/issues/3322
This PR addresses both problems by wrapping the quic.Stream that
is passed down to the proxying logic and handle all these concerns.
2022-01-27 22:37:45 +00:00
|
|
|
dest: "/slow_echo_body",
|
2021-08-03 07:09:56 +00:00
|
|
|
connectionType: quicpogs.ConnectionTypeHTTP,
|
|
|
|
metadata: []quicpogs.Metadata{
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpHeader:Cf-Ray",
|
|
|
|
Val: "123123123",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpHost",
|
|
|
|
Val: "cf.host",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpMethod",
|
|
|
|
Val: "POST",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpHeader:Content-Length",
|
|
|
|
Val: "24",
|
|
|
|
},
|
2021-08-03 07:09:56 +00:00
|
|
|
},
|
|
|
|
message: []byte("This is the message body"),
|
|
|
|
expectedResponse: []byte("This is the message body"),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "test ws proxy",
|
2021-10-19 19:01:17 +00:00
|
|
|
dest: "/ws/echo",
|
2021-08-03 09:04:02 +00:00
|
|
|
connectionType: quicpogs.ConnectionTypeWebsocket,
|
|
|
|
metadata: []quicpogs.Metadata{
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
|
|
|
|
Val: "Websocket",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpHeader:Another-Header",
|
|
|
|
Val: "Misc",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpHost",
|
|
|
|
Val: "cf.host",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-08-03 07:09:56 +00:00
|
|
|
Key: "HttpMethod",
|
|
|
|
Val: "get",
|
2021-08-03 09:04:02 +00:00
|
|
|
},
|
|
|
|
},
|
2021-08-03 07:09:56 +00:00
|
|
|
message: wsBuf.Bytes(),
|
2021-10-19 19:01:17 +00:00
|
|
|
expectedResponse: []byte{0x82, 0x5, 0x48, 0x65, 0x6c, 0x6c, 0x6f},
|
2021-08-03 09:04:02 +00:00
|
|
|
},
|
2021-08-17 14:30:02 +00:00
|
|
|
{
|
|
|
|
desc: "test tcp proxy",
|
|
|
|
connectionType: quicpogs.ConnectionTypeTCP,
|
|
|
|
metadata: []quicpogs.Metadata{},
|
|
|
|
message: []byte("Here is some tcp data"),
|
|
|
|
expectedResponse: []byte("Here is some tcp data"),
|
|
|
|
},
|
2021-08-03 09:04:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
var wg sync.WaitGroup
|
2021-10-21 14:48:11 +00:00
|
|
|
wg.Add(1)
|
2021-08-03 09:04:02 +00:00
|
|
|
go func() {
|
2021-08-03 07:09:56 +00:00
|
|
|
defer wg.Done()
|
2021-08-03 09:04:02 +00:00
|
|
|
quicServer(
|
2021-12-14 22:52:47 +00:00
|
|
|
t, udpListener, testTLSServerConfig, testQUICConfig,
|
2021-08-03 07:09:56 +00:00
|
|
|
test.dest, test.connectionType, test.metadata, test.message, test.expectedResponse,
|
2021-08-03 09:04:02 +00:00
|
|
|
)
|
|
|
|
}()
|
|
|
|
|
2022-01-05 16:01:56 +00:00
|
|
|
qc := testQUICConnection(udpListener.LocalAddr(), t)
|
2021-12-14 22:52:47 +00:00
|
|
|
go qc.Serve(ctx)
|
2021-08-03 09:04:02 +00:00
|
|
|
|
2021-08-03 07:09:56 +00:00
|
|
|
wg.Wait()
|
|
|
|
cancel()
|
2021-08-03 09:04:02 +00:00
|
|
|
})
|
|
|
|
}
|
2021-09-21 06:11:36 +00:00
|
|
|
}
|
2021-08-03 09:04:02 +00:00
|
|
|
|
2021-09-21 06:11:36 +00:00
|
|
|
type fakeControlStream struct {
|
|
|
|
ControlStreamHandler
|
|
|
|
}
|
|
|
|
|
2022-01-05 16:01:56 +00:00
|
|
|
func (fakeControlStream) ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions) error {
|
|
|
|
<-ctx.Done()
|
2021-09-21 06:11:36 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (fakeControlStream) IsStopped() bool {
|
|
|
|
return true
|
2021-08-03 09:04:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func quicServer(
|
|
|
|
t *testing.T,
|
|
|
|
conn net.PacketConn,
|
|
|
|
tlsConf *tls.Config,
|
|
|
|
config *quic.Config,
|
|
|
|
dest string,
|
|
|
|
connectionType quicpogs.ConnectionType,
|
|
|
|
metadata []quicpogs.Metadata,
|
|
|
|
message []byte,
|
|
|
|
expectedResponse []byte,
|
|
|
|
) {
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
defer cancel()
|
|
|
|
|
2021-08-17 14:30:02 +00:00
|
|
|
earlyListener, err := quic.Listen(conn, tlsConf, config)
|
2021-08-03 09:04:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
session, err := earlyListener.Accept(ctx)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
TUN-5621: Correctly manage QUIC stream closing
Until this PR, we were naively closing the quic.Stream whenever
the callstack for handling the request (HTTP or TCP) finished.
However, our proxy handler may still be reading or writing from
the quic.Stream at that point, because we return the callstack if
either side finishes, but not necessarily both.
This is a problem for quic-go library because quic.Stream#Close
cannot be called concurrently with quic.Stream#Write
Furthermore, we also noticed that quic.Stream#Close does nothing
to do receiving stream (since, underneath, quic.Stream has 2 streams,
1 for each direction), thus leaking memory, as explained in:
https://github.com/lucas-clemente/quic-go/issues/3322
This PR addresses both problems by wrapping the quic.Stream that
is passed down to the proxying logic and handle all these concerns.
2022-01-27 22:37:45 +00:00
|
|
|
quicStream, err := session.OpenStreamSync(context.Background())
|
2021-08-03 09:04:02 +00:00
|
|
|
require.NoError(t, err)
|
TUN-5621: Correctly manage QUIC stream closing
Until this PR, we were naively closing the quic.Stream whenever
the callstack for handling the request (HTTP or TCP) finished.
However, our proxy handler may still be reading or writing from
the quic.Stream at that point, because we return the callstack if
either side finishes, but not necessarily both.
This is a problem for quic-go library because quic.Stream#Close
cannot be called concurrently with quic.Stream#Write
Furthermore, we also noticed that quic.Stream#Close does nothing
to do receiving stream (since, underneath, quic.Stream has 2 streams,
1 for each direction), thus leaking memory, as explained in:
https://github.com/lucas-clemente/quic-go/issues/3322
This PR addresses both problems by wrapping the quic.Stream that
is passed down to the proxying logic and handle all these concerns.
2022-01-27 22:37:45 +00:00
|
|
|
stream := quicpogs.NewSafeStreamCloser(quicStream)
|
2021-08-03 09:04:02 +00:00
|
|
|
|
2021-11-12 09:37:28 +00:00
|
|
|
reqClientStream := quicpogs.RequestClientStream{ReadWriteCloser: stream}
|
|
|
|
err = reqClientStream.WriteConnectRequestData(dest, connectionType, metadata...)
|
2021-08-03 09:04:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-11-12 09:37:28 +00:00
|
|
|
_, err = reqClientStream.ReadConnectResponseData()
|
2021-08-03 09:04:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
if message != nil {
|
|
|
|
// ALPN successful. Write data.
|
TUN-5621: Correctly manage QUIC stream closing
Until this PR, we were naively closing the quic.Stream whenever
the callstack for handling the request (HTTP or TCP) finished.
However, our proxy handler may still be reading or writing from
the quic.Stream at that point, because we return the callstack if
either side finishes, but not necessarily both.
This is a problem for quic-go library because quic.Stream#Close
cannot be called concurrently with quic.Stream#Write
Furthermore, we also noticed that quic.Stream#Close does nothing
to do receiving stream (since, underneath, quic.Stream has 2 streams,
1 for each direction), thus leaking memory, as explained in:
https://github.com/lucas-clemente/quic-go/issues/3322
This PR addresses both problems by wrapping the quic.Stream that
is passed down to the proxying logic and handle all these concerns.
2022-01-27 22:37:45 +00:00
|
|
|
_, err := stream.Write(message)
|
2021-08-03 09:04:02 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
2021-08-03 07:09:56 +00:00
|
|
|
response := make([]byte, len(expectedResponse))
|
TUN-5621: Correctly manage QUIC stream closing
Until this PR, we were naively closing the quic.Stream whenever
the callstack for handling the request (HTTP or TCP) finished.
However, our proxy handler may still be reading or writing from
the quic.Stream at that point, because we return the callstack if
either side finishes, but not necessarily both.
This is a problem for quic-go library because quic.Stream#Close
cannot be called concurrently with quic.Stream#Write
Furthermore, we also noticed that quic.Stream#Close does nothing
to do receiving stream (since, underneath, quic.Stream has 2 streams,
1 for each direction), thus leaking memory, as explained in:
https://github.com/lucas-clemente/quic-go/issues/3322
This PR addresses both problems by wrapping the quic.Stream that
is passed down to the proxying logic and handle all these concerns.
2022-01-27 22:37:45 +00:00
|
|
|
_, err = stream.Read(response)
|
|
|
|
if err != io.EOF {
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
2021-08-03 09:04:02 +00:00
|
|
|
|
|
|
|
// For now it is an echo server. Verify if the same data is returned.
|
|
|
|
assert.Equal(t, expectedResponse, response)
|
|
|
|
}
|
|
|
|
|
2021-08-03 07:09:56 +00:00
|
|
|
type mockOriginProxyWithRequest struct{}
|
|
|
|
|
|
|
|
func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, r *http.Request, isWebsocket bool) error {
|
|
|
|
// These are a series of crude tests to ensure the headers and http related data is transferred from
|
|
|
|
// metadata.
|
|
|
|
if r.Method == "" {
|
|
|
|
return errors.New("method not sent")
|
|
|
|
}
|
|
|
|
if r.Host == "" {
|
|
|
|
return errors.New("host not sent")
|
|
|
|
}
|
|
|
|
if len(r.Header) == 0 {
|
|
|
|
return errors.New("headers not set")
|
|
|
|
}
|
|
|
|
|
|
|
|
if isWebsocket {
|
2021-10-19 19:01:17 +00:00
|
|
|
return wsEchoEndpoint(w, r)
|
2021-08-03 07:09:56 +00:00
|
|
|
}
|
|
|
|
switch r.URL.Path {
|
|
|
|
case "/ok":
|
|
|
|
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
|
TUN-5621: Correctly manage QUIC stream closing
Until this PR, we were naively closing the quic.Stream whenever
the callstack for handling the request (HTTP or TCP) finished.
However, our proxy handler may still be reading or writing from
the quic.Stream at that point, because we return the callstack if
either side finishes, but not necessarily both.
This is a problem for quic-go library because quic.Stream#Close
cannot be called concurrently with quic.Stream#Write
Furthermore, we also noticed that quic.Stream#Close does nothing
to do receiving stream (since, underneath, quic.Stream has 2 streams,
1 for each direction), thus leaking memory, as explained in:
https://github.com/lucas-clemente/quic-go/issues/3322
This PR addresses both problems by wrapping the quic.Stream that
is passed down to the proxying logic and handle all these concerns.
2022-01-27 22:37:45 +00:00
|
|
|
case "/slow_echo_body":
|
|
|
|
time.Sleep(5)
|
|
|
|
fallthrough
|
2021-08-03 07:09:56 +00:00
|
|
|
case "/echo_body":
|
|
|
|
resp := &http.Response{
|
|
|
|
StatusCode: http.StatusOK,
|
|
|
|
}
|
|
|
|
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
|
|
|
io.Copy(w, r.Body)
|
|
|
|
case "/error":
|
|
|
|
return fmt.Errorf("Failed to proxy to origin")
|
|
|
|
default:
|
|
|
|
originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-27 13:12:11 +00:00
|
|
|
func TestBuildHTTPRequest(t *testing.T) {
|
|
|
|
var tests = []struct {
|
|
|
|
name string
|
|
|
|
connectRequest *quicpogs.ConnectRequest
|
2021-10-07 14:47:27 +00:00
|
|
|
body io.ReadCloser
|
2021-09-27 13:12:11 +00:00
|
|
|
req *http.Request
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "check if http.Request is built correctly with content length",
|
|
|
|
connectRequest: &quicpogs.ConnectRequest{
|
|
|
|
Dest: "http://test.com",
|
|
|
|
Metadata: []quicpogs.Metadata{
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-09-27 13:12:11 +00:00
|
|
|
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
|
|
|
|
Val: "Websocket",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-09-27 13:12:11 +00:00
|
|
|
Key: "HttpHeader:Content-Length",
|
|
|
|
Val: "514",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-09-27 13:12:11 +00:00
|
|
|
Key: "HttpHeader:Another-Header",
|
|
|
|
Val: "Misc",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-09-27 13:12:11 +00:00
|
|
|
Key: "HttpHost",
|
|
|
|
Val: "cf.host",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-09-27 13:12:11 +00:00
|
|
|
Key: "HttpMethod",
|
|
|
|
Val: "get",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
req: &http.Request{
|
|
|
|
Method: "get",
|
|
|
|
URL: &url.URL{
|
|
|
|
Scheme: "http",
|
|
|
|
Host: "test.com",
|
|
|
|
},
|
|
|
|
Proto: "HTTP/1.1",
|
|
|
|
ProtoMajor: 1,
|
|
|
|
ProtoMinor: 1,
|
|
|
|
Header: http.Header{
|
|
|
|
"Another-Header": []string{"Misc"},
|
|
|
|
"Content-Length": []string{"514"},
|
|
|
|
},
|
|
|
|
ContentLength: 514,
|
|
|
|
Host: "cf.host",
|
2021-10-07 14:47:27 +00:00
|
|
|
Body: io.NopCloser(&bytes.Buffer{}),
|
2021-09-27 13:12:11 +00:00
|
|
|
},
|
2021-10-07 14:47:27 +00:00
|
|
|
body: io.NopCloser(&bytes.Buffer{}),
|
2021-09-27 13:12:11 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "if content length isn't part of request headers, then it's not set",
|
|
|
|
connectRequest: &quicpogs.ConnectRequest{
|
|
|
|
Dest: "http://test.com",
|
|
|
|
Metadata: []quicpogs.Metadata{
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-09-27 13:12:11 +00:00
|
|
|
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
|
|
|
|
Val: "Websocket",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-09-27 13:12:11 +00:00
|
|
|
Key: "HttpHeader:Another-Header",
|
|
|
|
Val: "Misc",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-09-27 13:12:11 +00:00
|
|
|
Key: "HttpHost",
|
|
|
|
Val: "cf.host",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-09-27 13:12:11 +00:00
|
|
|
Key: "HttpMethod",
|
|
|
|
Val: "get",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
req: &http.Request{
|
|
|
|
Method: "get",
|
|
|
|
URL: &url.URL{
|
|
|
|
Scheme: "http",
|
|
|
|
Host: "test.com",
|
|
|
|
},
|
|
|
|
Proto: "HTTP/1.1",
|
|
|
|
ProtoMajor: 1,
|
|
|
|
ProtoMinor: 1,
|
|
|
|
Header: http.Header{
|
|
|
|
"Another-Header": []string{"Misc"},
|
|
|
|
},
|
|
|
|
ContentLength: 0,
|
|
|
|
Host: "cf.host",
|
2021-10-07 14:47:27 +00:00
|
|
|
Body: nil,
|
|
|
|
},
|
|
|
|
body: io.NopCloser(&bytes.Buffer{}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "if content length is 0, but transfer-encoding is chunked, body is not nil",
|
|
|
|
connectRequest: &quicpogs.ConnectRequest{
|
|
|
|
Dest: "http://test.com",
|
|
|
|
Metadata: []quicpogs.Metadata{
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpHeader:Another-Header",
|
|
|
|
Val: "Misc",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpHeader:Transfer-Encoding",
|
|
|
|
Val: "chunked",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpHost",
|
|
|
|
Val: "cf.host",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpMethod",
|
|
|
|
Val: "get",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
req: &http.Request{
|
|
|
|
Method: "get",
|
|
|
|
URL: &url.URL{
|
|
|
|
Scheme: "http",
|
|
|
|
Host: "test.com",
|
|
|
|
},
|
|
|
|
Proto: "HTTP/1.1",
|
|
|
|
ProtoMajor: 1,
|
|
|
|
ProtoMinor: 1,
|
|
|
|
Header: http.Header{
|
|
|
|
"Another-Header": []string{"Misc"},
|
|
|
|
"Transfer-Encoding": []string{"chunked"},
|
|
|
|
},
|
|
|
|
ContentLength: 0,
|
|
|
|
Host: "cf.host",
|
|
|
|
Body: io.NopCloser(&bytes.Buffer{}),
|
|
|
|
},
|
|
|
|
body: io.NopCloser(&bytes.Buffer{}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "if content length is 0, but transfer-encoding is gzip,chunked, body is not nil",
|
|
|
|
connectRequest: &quicpogs.ConnectRequest{
|
|
|
|
Dest: "http://test.com",
|
|
|
|
Metadata: []quicpogs.Metadata{
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpHeader:Another-Header",
|
|
|
|
Val: "Misc",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpHeader:Transfer-Encoding",
|
|
|
|
Val: "gzip,chunked",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpHost",
|
|
|
|
Val: "cf.host",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpMethod",
|
|
|
|
Val: "get",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
req: &http.Request{
|
|
|
|
Method: "get",
|
|
|
|
URL: &url.URL{
|
|
|
|
Scheme: "http",
|
|
|
|
Host: "test.com",
|
|
|
|
},
|
|
|
|
Proto: "HTTP/1.1",
|
|
|
|
ProtoMajor: 1,
|
|
|
|
ProtoMinor: 1,
|
|
|
|
Header: http.Header{
|
|
|
|
"Another-Header": []string{"Misc"},
|
|
|
|
"Transfer-Encoding": []string{"gzip,chunked"},
|
|
|
|
},
|
|
|
|
ContentLength: 0,
|
|
|
|
Host: "cf.host",
|
|
|
|
Body: io.NopCloser(&bytes.Buffer{}),
|
|
|
|
},
|
|
|
|
body: io.NopCloser(&bytes.Buffer{}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "if content length is 0, and connect request is a websocket, body is not nil",
|
|
|
|
connectRequest: &quicpogs.ConnectRequest{
|
|
|
|
Type: quicpogs.ConnectionTypeWebsocket,
|
|
|
|
Dest: "http://test.com",
|
|
|
|
Metadata: []quicpogs.Metadata{
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpHeader:Another-Header",
|
|
|
|
Val: "Misc",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpHost",
|
|
|
|
Val: "cf.host",
|
|
|
|
},
|
2021-11-12 09:37:28 +00:00
|
|
|
{
|
2021-10-07 14:47:27 +00:00
|
|
|
Key: "HttpMethod",
|
|
|
|
Val: "get",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
req: &http.Request{
|
|
|
|
Method: "get",
|
|
|
|
URL: &url.URL{
|
|
|
|
Scheme: "http",
|
|
|
|
Host: "test.com",
|
|
|
|
},
|
|
|
|
Proto: "HTTP/1.1",
|
|
|
|
ProtoMajor: 1,
|
|
|
|
ProtoMinor: 1,
|
|
|
|
Header: http.Header{
|
|
|
|
"Another-Header": []string{"Misc"},
|
|
|
|
},
|
|
|
|
ContentLength: 0,
|
|
|
|
Host: "cf.host",
|
|
|
|
Body: io.NopCloser(&bytes.Buffer{}),
|
2021-09-27 13:12:11 +00:00
|
|
|
},
|
2021-10-07 14:47:27 +00:00
|
|
|
body: io.NopCloser(&bytes.Buffer{}),
|
2021-09-27 13:12:11 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
2021-10-07 14:47:27 +00:00
|
|
|
req, err := buildHTTPRequest(test.connectRequest, test.body)
|
2021-09-27 13:12:11 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
test.req = test.req.WithContext(req.Context())
|
|
|
|
assert.Equal(t, test.req, req)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-03 07:09:56 +00:00
|
|
|
func (moc *mockOriginProxyWithRequest) ProxyTCP(ctx context.Context, rwa ReadWriteAcker, tcpRequest *TCPRequest) error {
|
2021-08-17 14:30:02 +00:00
|
|
|
rwa.AckConnection()
|
|
|
|
io.Copy(rwa, rwa)
|
2021-08-03 07:09:56 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-12-14 22:52:47 +00:00
|
|
|
|
|
|
|
func TestServeUDPSession(t *testing.T) {
|
|
|
|
// Start a UDP Listener for QUIC.
|
|
|
|
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
|
|
|
require.NoError(t, err)
|
|
|
|
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer udpListener.Close()
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
|
|
|
// Establish QUIC connection with edge
|
|
|
|
edgeQUICSessionChan := make(chan quic.Session)
|
|
|
|
go func() {
|
|
|
|
earlyListener, err := quic.Listen(udpListener, testTLSServerConfig, testQUICConfig)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
edgeQUICSession, err := earlyListener.Accept(ctx)
|
|
|
|
require.NoError(t, err)
|
|
|
|
edgeQUICSessionChan <- edgeQUICSession
|
|
|
|
}()
|
|
|
|
|
2022-01-05 16:01:56 +00:00
|
|
|
qc := testQUICConnection(udpListener.LocalAddr(), t)
|
2021-12-14 22:52:47 +00:00
|
|
|
go qc.Serve(ctx)
|
|
|
|
|
|
|
|
edgeQUICSession := <-edgeQUICSessionChan
|
|
|
|
serveSession(ctx, qc, edgeQUICSession, closedByOrigin, io.EOF.Error(), t)
|
|
|
|
serveSession(ctx, qc, edgeQUICSession, closedByTimeout, datagramsession.SessionIdleErr(time.Millisecond*50).Error(), t)
|
|
|
|
serveSession(ctx, qc, edgeQUICSession, closedByRemote, "eyeball closed connection", t)
|
|
|
|
cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.Session, closeType closeReason, expectedReason string, t *testing.T) {
|
|
|
|
var (
|
|
|
|
payload = []byte(t.Name())
|
|
|
|
)
|
|
|
|
sessionID := uuid.New()
|
|
|
|
cfdConn, originConn := net.Pipe()
|
|
|
|
// Registers and run a new session
|
|
|
|
session, err := qc.sessionManager.RegisterSession(ctx, sessionID, cfdConn)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
sessionDone := make(chan struct{})
|
|
|
|
go func() {
|
|
|
|
qc.serveUDPSession(session, time.Millisecond*50)
|
|
|
|
close(sessionDone)
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Send a message to the quic session on edge side, it should be deumx to this datagram session
|
2022-01-06 12:17:10 +00:00
|
|
|
muxedPayload := append(payload, sessionID[:]...)
|
2021-12-14 22:52:47 +00:00
|
|
|
err = edgeQUICSession.SendMessage(muxedPayload)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
readBuffer := make([]byte, len(payload)+1)
|
|
|
|
n, err := originConn.Read(readBuffer)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, len(payload), n)
|
|
|
|
require.True(t, bytes.Equal(payload, readBuffer[:n]))
|
|
|
|
|
|
|
|
// Close connection to terminate session
|
|
|
|
switch closeType {
|
|
|
|
case closedByOrigin:
|
|
|
|
originConn.Close()
|
|
|
|
case closedByRemote:
|
|
|
|
err = qc.UnregisterUdpSession(ctx, sessionID, expectedReason)
|
|
|
|
require.NoError(t, err)
|
|
|
|
case closedByTimeout:
|
|
|
|
}
|
|
|
|
|
|
|
|
if closeType != closedByRemote {
|
|
|
|
// Session was not closed by remote, so closeUDPSession should be invoked to unregister from remote
|
|
|
|
unregisterFromEdgeChan := make(chan struct{})
|
2022-02-02 12:27:49 +00:00
|
|
|
sessionRPCServer := &mockSessionRPCServer{
|
2021-12-14 22:52:47 +00:00
|
|
|
sessionID: sessionID,
|
|
|
|
unregisterReason: expectedReason,
|
|
|
|
calledUnregisterChan: unregisterFromEdgeChan,
|
|
|
|
}
|
2022-02-02 12:27:49 +00:00
|
|
|
go runRPCServer(ctx, edgeQUICSession, sessionRPCServer, nil, t)
|
2021-12-14 22:52:47 +00:00
|
|
|
|
|
|
|
<-unregisterFromEdgeChan
|
|
|
|
}
|
|
|
|
|
|
|
|
<-sessionDone
|
|
|
|
}
|
|
|
|
|
|
|
|
type closeReason uint8
|
|
|
|
|
|
|
|
const (
|
|
|
|
closedByOrigin closeReason = iota
|
|
|
|
closedByRemote
|
|
|
|
closedByTimeout
|
|
|
|
)
|
|
|
|
|
2022-02-02 12:27:49 +00:00
|
|
|
func runRPCServer(ctx context.Context, session quic.Session, sessionRPCServer tunnelpogs.SessionManager, configRPCServer tunnelpogs.ConfigurationManager, t *testing.T) {
|
2021-12-14 22:52:47 +00:00
|
|
|
stream, err := session.AcceptStream(ctx)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
if stream.StreamID() == 0 {
|
|
|
|
// Skip the first stream, it's the control stream of the QUIC connection
|
|
|
|
stream, err = session.AcceptStream(ctx)
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
protocol, err := quicpogs.DetermineProtocol(stream)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
rpcServerStream, err := quicpogs.NewRPCServerStream(stream, protocol)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
log := zerolog.New(os.Stdout)
|
2022-02-02 12:27:49 +00:00
|
|
|
err = rpcServerStream.Serve(sessionRPCServer, configRPCServer, &log)
|
2021-12-14 22:52:47 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
type mockSessionRPCServer struct {
|
|
|
|
sessionID uuid.UUID
|
|
|
|
unregisterReason string
|
|
|
|
calledUnregisterChan chan struct{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s mockSessionRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error {
|
|
|
|
return fmt.Errorf("mockSessionRPCServer doesn't implement RegisterUdpSession")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s mockSessionRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, reason string) error {
|
|
|
|
if s.sessionID != sessionID {
|
|
|
|
return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID)
|
|
|
|
}
|
|
|
|
if s.unregisterReason != reason {
|
|
|
|
return fmt.Errorf("expect unregister reason %s, got %s", s.unregisterReason, reason)
|
|
|
|
}
|
|
|
|
close(s.calledUnregisterChan)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-01-05 16:01:56 +00:00
|
|
|
func testQUICConnection(udpListenerAddr net.Addr, t *testing.T) *QUICConnection {
|
2021-12-14 22:52:47 +00:00
|
|
|
tlsClientConfig := &tls.Config{
|
|
|
|
InsecureSkipVerify: true,
|
|
|
|
NextProtos: []string{"argotunnel"},
|
|
|
|
}
|
|
|
|
// Start a mock httpProxy
|
|
|
|
log := zerolog.New(os.Stdout)
|
|
|
|
qc, err := NewQUICConnection(
|
|
|
|
testQUICConfig,
|
|
|
|
udpListenerAddr,
|
|
|
|
tlsClientConfig,
|
2022-02-07 09:42:07 +00:00
|
|
|
&mockConfigManager{originProxy: &mockOriginProxyWithRequest{}},
|
2021-12-14 22:52:47 +00:00
|
|
|
&tunnelpogs.ConnectionOptions{},
|
|
|
|
fakeControlStream{},
|
2022-01-04 19:00:44 +00:00
|
|
|
&log,
|
2021-12-14 22:52:47 +00:00
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
return qc
|
|
|
|
}
|