From 81dff44bb9252356311f5feb0a7535fcd605bd7b Mon Sep 17 00:00:00 2001 From: Sudarsan Reddy Date: Thu, 8 Jul 2021 10:29:49 +0100 Subject: [PATCH] TUN-4596: Add QUIC application protocol for QUIC stream handshake - Vendored the capnproto library to cloudflared. - Added capnproto schema defining application protocol. - Added Pogs and application level read write of the protocol. --- Makefile | 6 + quic/pogs.go | 88 +++++ quic/quic_protocol.go | 115 ++++++ quic/quic_protocol_test.go | 88 +++++ quic/schema/quic_metadata_protocol.capnp | 28 ++ quic/schema/quic_metadata_protocol.capnp.go | 395 ++++++++++++++++++++ 6 files changed, 720 insertions(+) create mode 100644 quic/pogs.go create mode 100644 quic/quic_protocol.go create mode 100644 quic/quic_protocol_test.go create mode 100644 quic/schema/quic_metadata_protocol.capnp create mode 100644 quic/schema/quic_metadata_protocol.capnp.go diff --git a/Makefile b/Makefile index 82ed63db..eae3724c 100644 --- a/Makefile +++ b/Makefile @@ -252,6 +252,12 @@ tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp which capnpc-go # go get zombiezen.com/go/capnproto2/capnpc-go capnp compile -ogo tunnelrpc/tunnelrpc.capnp +.PHONY: quic-deps +quic-deps: + which capnp + which capnpc-go + capnp compile -ogo quic/schema/quic_metadata_protocol.capnp + .PHONY: vet vet: go vet -mod=vendor ./... diff --git a/quic/pogs.go b/quic/pogs.go new file mode 100644 index 00000000..4a11402e --- /dev/null +++ b/quic/pogs.go @@ -0,0 +1,88 @@ +package quic + +import ( + capnp "zombiezen.com/go/capnproto2" + "zombiezen.com/go/capnproto2/pogs" + + "github.com/cloudflare/cloudflared/quic/schema" +) + +// ConnectionType indicates the type of underlying connection proxied within the QUIC stream. +type ConnectionType uint16 + +const ( + ConnectionTypeHTTP ConnectionType = iota + ConnectionTypeWebsocket + ConnectionTypeTCP +) + +// ConnectRequest is the representation of metadata sent at the start of a QUIC application handshake. +type ConnectRequest struct { + Dest string `capnp:"dest"` + Type ConnectionType `capnp:"type"` + Metadata []Metadata `capnp:"metadata"` +} + +// Metadata is a representation of key value based data sent via RequestMeta. +type Metadata struct { + Key string `capnp:"key"` + Val string `capnp:"val"` +} + +func (r *ConnectRequest) fromPogs(msg *capnp.Message) error { + metadata, err := schema.ReadRootConnectRequest(msg) + if err != nil { + return err + } + return pogs.Extract(r, schema.ConnectRequest_TypeID, metadata.Struct) +} + +func (r *ConnectRequest) toPogs() (*capnp.Message, error) { + msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) + if err != nil { + return nil, err + } + + root, err := schema.NewRootConnectRequest(seg) + if err != nil { + return nil, err + } + + if err := pogs.Insert(schema.ConnectRequest_TypeID, root.Struct, r); err != nil { + return nil, err + } + + return msg, nil +} + +// ConnectResponse is a representation of metadata sent as a response to a QUIC application handshake. +type ConnectResponse struct { + Error string `capnp:"error"` + Metadata []Metadata `capnp:"metadata"` +} + +func (r *ConnectResponse) fromPogs(msg *capnp.Message) error { + metadata, err := schema.ReadRootConnectResponse(msg) + if err != nil { + return err + } + return pogs.Extract(r, schema.ConnectResponse_TypeID, metadata.Struct) +} + +func (r *ConnectResponse) toPogs() (*capnp.Message, error) { + msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) + if err != nil { + return nil, err + } + + root, err := schema.NewRootConnectResponse(seg) + if err != nil { + return nil, err + } + + if err := pogs.Insert(schema.ConnectResponse_TypeID, root.Struct, r); err != nil { + return nil, err + } + + return msg, nil +} diff --git a/quic/quic_protocol.go b/quic/quic_protocol.go new file mode 100644 index 00000000..2d1f8e7e --- /dev/null +++ b/quic/quic_protocol.go @@ -0,0 +1,115 @@ +package quic + +import ( + "bytes" + "fmt" + "io" + + capnp "zombiezen.com/go/capnproto2" +) + +// protocolSignature is a custom protocol signature to ensure that whoever performs a handshake does not write data +// before writing the metadata. +var protocolSignature = []byte{0x0A, 0x36, 0xCD, 0x12, 0xA1, 0x3E} + +// ReadConnectRequestData reads the handshake data from a QUIC stream. +func ReadConnectRequestData(stream io.Reader) (*ConnectRequest, error) { + if err := verifySignature(stream); err != nil { + return nil, err + } + + msg, err := capnp.NewDecoder(stream).Decode() + if err != nil { + return nil, err + } + + r := &ConnectRequest{} + if err := r.fromPogs(msg); err != nil { + return nil, err + } + return r, nil +} + +// WriteConnectRequestData writes requestMeta to a stream. +func WriteConnectRequestData(stream io.Writer, dest string, connectionType ConnectionType, metadata ...Metadata) error { + connectRequest := &ConnectRequest{ + Dest: dest, + Type: connectionType, + Metadata: metadata, + } + + msg, err := connectRequest.toPogs() + if err != nil { + return err + } + + if err := writePreamble(stream); err != nil { + return err + } + return capnp.NewEncoder(stream).Encode(msg) +} + +// ReadConnectResponseData reads the response to a RequestMeta in a stream. +func ReadConnectResponseData(stream io.Reader) (*ConnectResponse, error) { + if err := verifySignature(stream); err != nil { + return nil, err + } + + msg, err := capnp.NewDecoder(stream).Decode() + if err != nil { + return nil, err + } + + r := &ConnectResponse{} + if err := r.fromPogs(msg); err != nil { + return nil, err + } + return r, nil +} + +// WriteConnectResponseData writes response to a QUIC stream. +func WriteConnectResponseData(stream io.Writer, respErr error, metadata ...Metadata) error { + var connectResponse *ConnectResponse + if respErr != nil { + connectResponse = &ConnectResponse{ + Error: respErr.Error(), + } + } else { + connectResponse = &ConnectResponse{ + Metadata: metadata, + } + } + + msg, err := connectResponse.toPogs() + if err != nil { + return err + } + + if err := writePreamble(stream); err != nil { + return err + } + return capnp.NewEncoder(stream).Encode(msg) +} + +func writePreamble(stream io.Writer) error { + return writeSignature(stream) + // TODO : TUN-4613 Write protocol version here +} + +func writeSignature(stream io.Writer) error { + _, err := stream.Write(protocolSignature) + return err +} + +func verifySignature(stream io.Reader) error { + signature := make([]byte, len(protocolSignature)) + if _, err := io.ReadFull(stream, signature); err != nil { + return err + } + + if !bytes.Equal(signature[0:], protocolSignature) { + return fmt.Errorf("Wrong signature: %v", signature) + } + + return nil +} diff --git a/quic/quic_protocol_test.go b/quic/quic_protocol_test.go new file mode 100644 index 00000000..0856d07b --- /dev/null +++ b/quic/quic_protocol_test.go @@ -0,0 +1,88 @@ +package quic + +import ( + "bytes" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConnectRequestData(t *testing.T) { + var tests = []struct { + name string + hostname string + connectionType ConnectionType + metadata []Metadata + }{ + { + name: "Signature verified and request metadata is unmarshaled and read correctly", + hostname: "tunnel.com", + connectionType: ConnectionTypeHTTP, + metadata: []Metadata{ + Metadata{ + Key: "key", + Val: "1234", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + b := &bytes.Buffer{} + err := WriteConnectRequestData(b, test.hostname, test.connectionType, test.metadata...) + require.NoError(t, err) + reqMeta, err := ReadConnectRequestData(b) + require.NoError(t, err) + + assert.Equal(t, test.metadata, reqMeta.Metadata) + assert.Equal(t, test.hostname, reqMeta.Dest) + assert.Equal(t, test.connectionType, reqMeta.Type) + }) + } +} + +func TestConnectResponseMeta(t *testing.T) { + var tests = []struct { + name string + err error + metadata []Metadata + }{ + { + name: "Signature verified and response metadata is unmarshaled and read correctly", + metadata: []Metadata{ + Metadata{ + Key: "key", + Val: "1234", + }, + }, + }, + { + name: "If error is not empty, other fields should be blank", + err: errors.New("something happened"), + metadata: []Metadata{ + Metadata{ + Key: "key", + Val: "1234", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + b := &bytes.Buffer{} + err := WriteConnectResponseData(b, test.err, test.metadata...) + require.NoError(t, err) + respMeta, err := ReadConnectResponseData(b) + require.NoError(t, err) + + if respMeta.Error == "" { + assert.Equal(t, test.metadata, respMeta.Metadata) + } else { + assert.Equal(t, 0, len(respMeta.Metadata)) + } + }) + } +} diff --git a/quic/schema/quic_metadata_protocol.capnp b/quic/schema/quic_metadata_protocol.capnp new file mode 100644 index 00000000..db533716 --- /dev/null +++ b/quic/schema/quic_metadata_protocol.capnp @@ -0,0 +1,28 @@ +using Go = import "/go.capnp"; +@0xb29021ef7421cc32; + +$Go.package("schema"); +$Go.import("schema"); + + +struct ConnectRequest{ + dest @0 :Text; + type @1 :ConnectionType; + metadata @2 :List(Metadata); +} + +enum ConnectionType{ + http @0; + websocket @1; + tcp @2; +} + +struct Metadata { + key @0 :Text; + val @1 :Text; +} + +struct ConnectResponse{ + error @0 :Text; + metadata @1 :List(Metadata); +} diff --git a/quic/schema/quic_metadata_protocol.capnp.go b/quic/schema/quic_metadata_protocol.capnp.go new file mode 100644 index 00000000..2d22b8a6 --- /dev/null +++ b/quic/schema/quic_metadata_protocol.capnp.go @@ -0,0 +1,395 @@ +// Code generated by capnpc-go. DO NOT EDIT. + +package schema + +import ( + capnp "zombiezen.com/go/capnproto2" + text "zombiezen.com/go/capnproto2/encoding/text" + schemas "zombiezen.com/go/capnproto2/schemas" +) + +type ConnectRequest struct{ capnp.Struct } + +// ConnectRequest_TypeID is the unique identifier for the type ConnectRequest. +const ConnectRequest_TypeID = 0xc47116a1045e4061 + +func NewConnectRequest(s *capnp.Segment) (ConnectRequest, error) { + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}) + return ConnectRequest{st}, err +} + +func NewRootConnectRequest(s *capnp.Segment) (ConnectRequest, error) { + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}) + return ConnectRequest{st}, err +} + +func ReadRootConnectRequest(msg *capnp.Message) (ConnectRequest, error) { + root, err := msg.RootPtr() + return ConnectRequest{root.Struct()}, err +} + +func (s ConnectRequest) String() string { + str, _ := text.Marshal(0xc47116a1045e4061, s.Struct) + return str +} + +func (s ConnectRequest) Dest() (string, error) { + p, err := s.Struct.Ptr(0) + return p.Text(), err +} + +func (s ConnectRequest) HasDest() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s ConnectRequest) DestBytes() ([]byte, error) { + p, err := s.Struct.Ptr(0) + return p.TextBytes(), err +} + +func (s ConnectRequest) SetDest(v string) error { + return s.Struct.SetText(0, v) +} + +func (s ConnectRequest) Type() ConnectionType { + return ConnectionType(s.Struct.Uint16(0)) +} + +func (s ConnectRequest) SetType(v ConnectionType) { + s.Struct.SetUint16(0, uint16(v)) +} + +func (s ConnectRequest) Metadata() (Metadata_List, error) { + p, err := s.Struct.Ptr(1) + return Metadata_List{List: p.List()}, err +} + +func (s ConnectRequest) HasMetadata() bool { + p, err := s.Struct.Ptr(1) + return p.IsValid() || err != nil +} + +func (s ConnectRequest) SetMetadata(v Metadata_List) error { + return s.Struct.SetPtr(1, v.List.ToPtr()) +} + +// NewMetadata sets the metadata field to a newly +// allocated Metadata_List, preferring placement in s's segment. +func (s ConnectRequest) NewMetadata(n int32) (Metadata_List, error) { + l, err := NewMetadata_List(s.Struct.Segment(), n) + if err != nil { + return Metadata_List{}, err + } + err = s.Struct.SetPtr(1, l.List.ToPtr()) + return l, err +} + +// ConnectRequest_List is a list of ConnectRequest. +type ConnectRequest_List struct{ capnp.List } + +// NewConnectRequest creates a new list of ConnectRequest. +func NewConnectRequest_List(s *capnp.Segment, sz int32) (ConnectRequest_List, error) { + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}, sz) + return ConnectRequest_List{l}, err +} + +func (s ConnectRequest_List) At(i int) ConnectRequest { return ConnectRequest{s.List.Struct(i)} } + +func (s ConnectRequest_List) Set(i int, v ConnectRequest) error { return s.List.SetStruct(i, v.Struct) } + +func (s ConnectRequest_List) String() string { + str, _ := text.MarshalList(0xc47116a1045e4061, s.List) + return str +} + +// ConnectRequest_Promise is a wrapper for a ConnectRequest promised by a client call. +type ConnectRequest_Promise struct{ *capnp.Pipeline } + +func (p ConnectRequest_Promise) Struct() (ConnectRequest, error) { + s, err := p.Pipeline.Struct() + return ConnectRequest{s}, err +} + +type ConnectionType uint16 + +// ConnectionType_TypeID is the unique identifier for the type ConnectionType. +const ConnectionType_TypeID = 0xc52e1bac26d379c8 + +// Values of ConnectionType. +const ( + ConnectionType_http ConnectionType = 0 + ConnectionType_websocket ConnectionType = 1 + ConnectionType_tcp ConnectionType = 2 +) + +// String returns the enum's constant name. +func (c ConnectionType) String() string { + switch c { + case ConnectionType_http: + return "http" + case ConnectionType_websocket: + return "websocket" + case ConnectionType_tcp: + return "tcp" + + default: + return "" + } +} + +// ConnectionTypeFromString returns the enum value with a name, +// or the zero value if there's no such value. +func ConnectionTypeFromString(c string) ConnectionType { + switch c { + case "http": + return ConnectionType_http + case "websocket": + return ConnectionType_websocket + case "tcp": + return ConnectionType_tcp + + default: + return 0 + } +} + +type ConnectionType_List struct{ capnp.List } + +func NewConnectionType_List(s *capnp.Segment, sz int32) (ConnectionType_List, error) { + l, err := capnp.NewUInt16List(s, sz) + return ConnectionType_List{l.List}, err +} + +func (l ConnectionType_List) At(i int) ConnectionType { + ul := capnp.UInt16List{List: l.List} + return ConnectionType(ul.At(i)) +} + +func (l ConnectionType_List) Set(i int, v ConnectionType) { + ul := capnp.UInt16List{List: l.List} + ul.Set(i, uint16(v)) +} + +type Metadata struct{ capnp.Struct } + +// Metadata_TypeID is the unique identifier for the type Metadata. +const Metadata_TypeID = 0xe1446b97bfd1cd37 + +func NewMetadata(s *capnp.Segment) (Metadata, error) { + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) + return Metadata{st}, err +} + +func NewRootMetadata(s *capnp.Segment) (Metadata, error) { + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) + return Metadata{st}, err +} + +func ReadRootMetadata(msg *capnp.Message) (Metadata, error) { + root, err := msg.RootPtr() + return Metadata{root.Struct()}, err +} + +func (s Metadata) String() string { + str, _ := text.Marshal(0xe1446b97bfd1cd37, s.Struct) + return str +} + +func (s Metadata) Key() (string, error) { + p, err := s.Struct.Ptr(0) + return p.Text(), err +} + +func (s Metadata) HasKey() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s Metadata) KeyBytes() ([]byte, error) { + p, err := s.Struct.Ptr(0) + return p.TextBytes(), err +} + +func (s Metadata) SetKey(v string) error { + return s.Struct.SetText(0, v) +} + +func (s Metadata) Val() (string, error) { + p, err := s.Struct.Ptr(1) + return p.Text(), err +} + +func (s Metadata) HasVal() bool { + p, err := s.Struct.Ptr(1) + return p.IsValid() || err != nil +} + +func (s Metadata) ValBytes() ([]byte, error) { + p, err := s.Struct.Ptr(1) + return p.TextBytes(), err +} + +func (s Metadata) SetVal(v string) error { + return s.Struct.SetText(1, v) +} + +// Metadata_List is a list of Metadata. +type Metadata_List struct{ capnp.List } + +// NewMetadata creates a new list of Metadata. +func NewMetadata_List(s *capnp.Segment, sz int32) (Metadata_List, error) { + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) + return Metadata_List{l}, err +} + +func (s Metadata_List) At(i int) Metadata { return Metadata{s.List.Struct(i)} } + +func (s Metadata_List) Set(i int, v Metadata) error { return s.List.SetStruct(i, v.Struct) } + +func (s Metadata_List) String() string { + str, _ := text.MarshalList(0xe1446b97bfd1cd37, s.List) + return str +} + +// Metadata_Promise is a wrapper for a Metadata promised by a client call. +type Metadata_Promise struct{ *capnp.Pipeline } + +func (p Metadata_Promise) Struct() (Metadata, error) { + s, err := p.Pipeline.Struct() + return Metadata{s}, err +} + +type ConnectResponse struct{ capnp.Struct } + +// ConnectResponse_TypeID is the unique identifier for the type ConnectResponse. +const ConnectResponse_TypeID = 0xb1032ec91cef8727 + +func NewConnectResponse(s *capnp.Segment) (ConnectResponse, error) { + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) + return ConnectResponse{st}, err +} + +func NewRootConnectResponse(s *capnp.Segment) (ConnectResponse, error) { + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) + return ConnectResponse{st}, err +} + +func ReadRootConnectResponse(msg *capnp.Message) (ConnectResponse, error) { + root, err := msg.RootPtr() + return ConnectResponse{root.Struct()}, err +} + +func (s ConnectResponse) String() string { + str, _ := text.Marshal(0xb1032ec91cef8727, s.Struct) + return str +} + +func (s ConnectResponse) Error() (string, error) { + p, err := s.Struct.Ptr(0) + return p.Text(), err +} + +func (s ConnectResponse) HasError() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s ConnectResponse) ErrorBytes() ([]byte, error) { + p, err := s.Struct.Ptr(0) + return p.TextBytes(), err +} + +func (s ConnectResponse) SetError(v string) error { + return s.Struct.SetText(0, v) +} + +func (s ConnectResponse) Metadata() (Metadata_List, error) { + p, err := s.Struct.Ptr(1) + return Metadata_List{List: p.List()}, err +} + +func (s ConnectResponse) HasMetadata() bool { + p, err := s.Struct.Ptr(1) + return p.IsValid() || err != nil +} + +func (s ConnectResponse) SetMetadata(v Metadata_List) error { + return s.Struct.SetPtr(1, v.List.ToPtr()) +} + +// NewMetadata sets the metadata field to a newly +// allocated Metadata_List, preferring placement in s's segment. +func (s ConnectResponse) NewMetadata(n int32) (Metadata_List, error) { + l, err := NewMetadata_List(s.Struct.Segment(), n) + if err != nil { + return Metadata_List{}, err + } + err = s.Struct.SetPtr(1, l.List.ToPtr()) + return l, err +} + +// ConnectResponse_List is a list of ConnectResponse. +type ConnectResponse_List struct{ capnp.List } + +// NewConnectResponse creates a new list of ConnectResponse. +func NewConnectResponse_List(s *capnp.Segment, sz int32) (ConnectResponse_List, error) { + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) + return ConnectResponse_List{l}, err +} + +func (s ConnectResponse_List) At(i int) ConnectResponse { return ConnectResponse{s.List.Struct(i)} } + +func (s ConnectResponse_List) Set(i int, v ConnectResponse) error { + return s.List.SetStruct(i, v.Struct) +} + +func (s ConnectResponse_List) String() string { + str, _ := text.MarshalList(0xb1032ec91cef8727, s.List) + return str +} + +// ConnectResponse_Promise is a wrapper for a ConnectResponse promised by a client call. +type ConnectResponse_Promise struct{ *capnp.Pipeline } + +func (p ConnectResponse_Promise) Struct() (ConnectResponse, error) { + s, err := p.Pipeline.Struct() + return ConnectResponse{s}, err +} + +const schema_b29021ef7421cc32 = "x\xda\xb4\x91\xcfk\x13A\x1c\xc5\xdf\x9bI\xba\x1e\xa2" + + "\x9b!\xd5\x8b\x8a\xa4\xf8+ES\xdb(\xa2\xa7\x80\x15" + + "TZ\xcc\x14\xcf\x96u;\x98\x92vw\x92\x9dZ\xf2" + + "\x17x\x15/\xe2\xd1\xbb \x15<\x0b\xa2\xa0\xa2\x07\x11" + + "\xff\x80\xfe\x05=y\xf0\xb42)\xdb@)\x08Bo" + + "\xdfy<\xe6}\xbe\xdfW\xfd\xd5\x16\xb3\xe5\xc7\x04t" + + "\xb5<\x91_x\xbas\xeaKSnA5\x98\xcf}" + + "\xab\xbb\x9d\xfa\xb3\xb7(\x8b\x00\x98}\xf9\x95\xea]\x00" + + "\xa8\xadM0\x8f\xda\x0fK\xafN\xf4?B7\xb8\xdf" + + "\xda\xaa\xf3\x03k7\x18\x00\xb5k|\x03\xe6\x9f\x87?" + + "\xcf\xbf>\xd9\xfc\x04\xd5\x10c3\xd8\xda\xf6\xce?#" + + "\xe7o\xde\x07\xf3\xeb\xdf\x7f\xbc\x7f\xd1\x9b\xdf>\x80\xa0" + + "uT