TUN-1606: Define CloudflaredConfig RPC structure, interface for cloudflared's RPC server

This commit is contained in:
Nick Vollmar
2019-03-29 18:05:11 -05:00
parent 9a43a92b1c
commit 13d25a52a9
7 changed files with 2354 additions and 138 deletions

373
tunnelrpc/pogs/config.go Normal file
View File

@@ -0,0 +1,373 @@
package pogs
import (
"context"
"fmt"
"time"
"github.com/cloudflare/cloudflared/tunnelrpc"
capnp "zombiezen.com/go/capnproto2"
"zombiezen.com/go/capnproto2/pogs"
"zombiezen.com/go/capnproto2/rpc"
)
///
/// Structs
///
type CloudflaredConfig struct {
Timestamp time.Time
AutoUpdateFrequency time.Duration
MetricsUpdateFrequency time.Duration
HeartbeatInterval time.Duration
MaxFailedHeartbeats uint64
GracePeriod time.Duration
DoHProxyConfigs []*DoHProxyConfig
ReverseProxyConfigs []*ReverseProxyConfig
}
type UseConfigurationResult struct {
Success bool
ErrorMessage string
}
type DoHProxyConfig struct {
ListenHost string
ListenPort uint16
Upstreams []string
}
type ReverseProxyConfig struct {
TunnelID string
Origin OriginConfig
Retries uint64
ConnectionTimeout time.Duration
ChunkedEncoding bool
CompressionQuality uint64
}
//go-sumtype:decl OriginConfig
type OriginConfig interface {
isOriginConfig()
}
type HTTPOriginConfig struct {
URL string `capnp:"url"`
TCPKeepAlive time.Duration `capnp:"tcpKeepAlive"`
DialDualStack bool
TLSHandshakeTimeout time.Duration `capnp:"tlsHandshakeTimeout"`
TLSVerify bool `capnp:"tlsVerify"`
OriginCAPool string
OriginServerName string
MaxIdleConnections uint64
IdleConnectionTimeout time.Duration
}
func (_ *HTTPOriginConfig) isOriginConfig() {}
type UnixSocketOriginConfig struct {
Path string
}
func (_ *UnixSocketOriginConfig) isOriginConfig() {}
type WebSocketOriginConfig struct {
URL string `capnp:"url"`
}
func (_ *WebSocketOriginConfig) isOriginConfig() {}
type HelloWorldOriginConfig struct{}
func (_ *HelloWorldOriginConfig) isOriginConfig() {}
///
/// Boilerplate to convert between these structs and the primitive structs generated by capnp-go
///
func MarshalCloudflaredConfig(s tunnelrpc.CloudflaredConfig, p *CloudflaredConfig) error {
s.SetTimestamp(p.Timestamp.UnixNano())
s.SetAutoUpdateFrequency(p.AutoUpdateFrequency.Nanoseconds())
s.SetMetricsUpdateFrequency(p.MetricsUpdateFrequency.Nanoseconds())
s.SetHeartbeatInterval(p.HeartbeatInterval.Nanoseconds())
s.SetMaxFailedHeartbeats(p.MaxFailedHeartbeats)
s.SetGracePeriod(p.GracePeriod.Nanoseconds())
err := marshalDoHProxyConfigs(s, p.DoHProxyConfigs)
if err != nil {
return err
}
err = marshalReverseProxyConfigs(s, p.ReverseProxyConfigs)
if err != nil {
return err
}
return nil
}
func marshalDoHProxyConfigs(s tunnelrpc.CloudflaredConfig, dohProxyConfigs []*DoHProxyConfig) error {
capnpList, err := s.NewDohProxyConfigs(int32(len(dohProxyConfigs)))
if err != nil {
return err
}
for i, unmarshalledConfig := range dohProxyConfigs {
err := MarshalDoHProxyConfig(capnpList.At(i), unmarshalledConfig)
if err != nil {
return err
}
}
return nil
}
func marshalReverseProxyConfigs(s tunnelrpc.CloudflaredConfig, reverseProxyConfigs []*ReverseProxyConfig) error {
capnpList, err := s.NewReverseProxyConfigs(int32(len(reverseProxyConfigs)))
if err != nil {
return err
}
for i, unmarshalledConfig := range reverseProxyConfigs {
err := MarshalReverseProxyConfig(capnpList.At(i), unmarshalledConfig)
if err != nil {
return err
}
}
return nil
}
func UnmarshalCloudflaredConfig(s tunnelrpc.CloudflaredConfig) (*CloudflaredConfig, error) {
p := new(CloudflaredConfig)
p.Timestamp = time.Unix(0, s.Timestamp()).UTC()
p.AutoUpdateFrequency = time.Duration(s.AutoUpdateFrequency())
p.MetricsUpdateFrequency = time.Duration(s.MetricsUpdateFrequency())
p.HeartbeatInterval = time.Duration(s.HeartbeatInterval())
p.MaxFailedHeartbeats = s.MaxFailedHeartbeats()
p.GracePeriod = time.Duration(s.GracePeriod())
dohProxyConfigs, err := unmarshalDoHProxyConfigs(s)
if err != nil {
return nil, err
}
p.DoHProxyConfigs = dohProxyConfigs
reverseProxyConfigs, err := unmarshalReverseProxyConfigs(s)
if err != nil {
return nil, err
}
p.ReverseProxyConfigs = reverseProxyConfigs
return p, err
}
func unmarshalDoHProxyConfigs(s tunnelrpc.CloudflaredConfig) ([]*DoHProxyConfig, error) {
var result []*DoHProxyConfig
marshalledDoHProxyConfigs, err := s.DohProxyConfigs()
if err != nil {
return nil, err
}
for i := 0; i < marshalledDoHProxyConfigs.Len(); i++ {
ss := marshalledDoHProxyConfigs.At(i)
dohProxyConfig, err := UnmarshalDoHProxyConfig(ss)
if err != nil {
return nil, err
}
result = append(result, dohProxyConfig)
}
return result, nil
}
func unmarshalReverseProxyConfigs(s tunnelrpc.CloudflaredConfig) ([]*ReverseProxyConfig, error) {
var result []*ReverseProxyConfig
marshalledReverseProxyConfigs, err := s.ReverseProxyConfigs()
if err != nil {
return nil, err
}
for i := 0; i < marshalledReverseProxyConfigs.Len(); i++ {
ss := marshalledReverseProxyConfigs.At(i)
reverseProxyConfig, err := UnmarshalReverseProxyConfig(ss)
if err != nil {
return nil, err
}
result = append(result, reverseProxyConfig)
}
return result, nil
}
func MarshalUseConfigurationResult(s tunnelrpc.UseConfigurationResult, p *UseConfigurationResult) error {
return pogs.Insert(tunnelrpc.UseConfigurationResult_TypeID, s.Struct, p)
}
func UnmarshalUseConfigurationResult(s tunnelrpc.UseConfigurationResult) (*UseConfigurationResult, error) {
p := new(UseConfigurationResult)
err := pogs.Extract(p, tunnelrpc.UseConfigurationResult_TypeID, s.Struct)
return p, err
}
func MarshalDoHProxyConfig(s tunnelrpc.DoHProxyConfig, p *DoHProxyConfig) error {
return pogs.Insert(tunnelrpc.DoHProxyConfig_TypeID, s.Struct, p)
}
func UnmarshalDoHProxyConfig(s tunnelrpc.DoHProxyConfig) (*DoHProxyConfig, error) {
p := new(DoHProxyConfig)
err := pogs.Extract(p, tunnelrpc.DoHProxyConfig_TypeID, s.Struct)
return p, err
}
func MarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig, p *ReverseProxyConfig) error {
s.SetTunnelID(p.TunnelID)
switch config := p.Origin.(type) {
case *HTTPOriginConfig:
ss, err := s.Origin().NewHttp()
if err != nil {
return err
}
MarshalHTTPOriginConfig(ss, config)
case *UnixSocketOriginConfig:
ss, err := s.Origin().NewSocket()
if err != nil {
return err
}
MarshalUnixSocketOriginConfig(ss, config)
case *WebSocketOriginConfig:
ss, err := s.Origin().NewWebsocket()
if err != nil {
return err
}
MarshalWebSocketOriginConfig(ss, config)
case *HelloWorldOriginConfig:
ss, err := s.Origin().NewHelloWorld()
if err != nil {
return err
}
MarshalHelloWorldOriginConfig(ss, config)
default:
return fmt.Errorf("Unknown type for config: %T", config)
}
s.SetRetries(p.Retries)
s.SetConnectionTimeout(p.ConnectionTimeout.Nanoseconds())
s.SetChunkedEncoding(p.ChunkedEncoding)
s.SetCompressionQuality(p.CompressionQuality)
return nil
}
func UnmarshalReverseProxyConfig(s tunnelrpc.ReverseProxyConfig) (*ReverseProxyConfig, error) {
p := new(ReverseProxyConfig)
tunnelID, err := s.TunnelID()
if err != nil {
return nil, err
}
p.TunnelID = tunnelID
switch s.Origin().Which() {
case tunnelrpc.ReverseProxyConfig_origin_Which_http:
ss, err := s.Origin().Http()
if err != nil {
return nil, err
}
config, err := UnmarshalHTTPOriginConfig(ss)
if err != nil {
return nil, err
}
p.Origin = config
case tunnelrpc.ReverseProxyConfig_origin_Which_socket:
ss, err := s.Origin().Socket()
if err != nil {
return nil, err
}
config, err := UnmarshalUnixSocketOriginConfig(ss)
if err != nil {
return nil, err
}
p.Origin = config
case tunnelrpc.ReverseProxyConfig_origin_Which_websocket:
ss, err := s.Origin().Websocket()
if err != nil {
return nil, err
}
config, err := UnmarshalWebSocketOriginConfig(ss)
if err != nil {
return nil, err
}
p.Origin = config
case tunnelrpc.ReverseProxyConfig_origin_Which_helloWorld:
ss, err := s.Origin().HelloWorld()
if err != nil {
return nil, err
}
config, err := UnmarshalHelloWorldOriginConfig(ss)
if err != nil {
return nil, err
}
p.Origin = config
}
p.Retries = s.Retries()
p.ConnectionTimeout = time.Duration(s.ConnectionTimeout())
p.ChunkedEncoding = s.ChunkedEncoding()
p.CompressionQuality = s.CompressionQuality()
return p, nil
}
func MarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig, p *HTTPOriginConfig) error {
return pogs.Insert(tunnelrpc.HTTPOriginConfig_TypeID, s.Struct, p)
}
func UnmarshalHTTPOriginConfig(s tunnelrpc.HTTPOriginConfig) (*HTTPOriginConfig, error) {
p := new(HTTPOriginConfig)
err := pogs.Extract(p, tunnelrpc.HTTPOriginConfig_TypeID, s.Struct)
return p, err
}
func MarshalUnixSocketOriginConfig(s tunnelrpc.UnixSocketOriginConfig, p *UnixSocketOriginConfig) error {
return pogs.Insert(tunnelrpc.UnixSocketOriginConfig_TypeID, s.Struct, p)
}
func UnmarshalUnixSocketOriginConfig(s tunnelrpc.UnixSocketOriginConfig) (*UnixSocketOriginConfig, error) {
p := new(UnixSocketOriginConfig)
err := pogs.Extract(p, tunnelrpc.UnixSocketOriginConfig_TypeID, s.Struct)
return p, err
}
func MarshalWebSocketOriginConfig(s tunnelrpc.WebSocketOriginConfig, p *WebSocketOriginConfig) error {
return pogs.Insert(tunnelrpc.WebSocketOriginConfig_TypeID, s.Struct, p)
}
func UnmarshalWebSocketOriginConfig(s tunnelrpc.WebSocketOriginConfig) (*WebSocketOriginConfig, error) {
p := new(WebSocketOriginConfig)
err := pogs.Extract(p, tunnelrpc.WebSocketOriginConfig_TypeID, s.Struct)
return p, err
}
func MarshalHelloWorldOriginConfig(s tunnelrpc.HelloWorldOriginConfig, p *HelloWorldOriginConfig) error {
return pogs.Insert(tunnelrpc.HelloWorldOriginConfig_TypeID, s.Struct, p)
}
func UnmarshalHelloWorldOriginConfig(s tunnelrpc.HelloWorldOriginConfig) (*HelloWorldOriginConfig, error) {
p := new(HelloWorldOriginConfig)
err := pogs.Extract(p, tunnelrpc.HelloWorldOriginConfig_TypeID, s.Struct)
return p, err
}
type CloudflaredServer interface {
UseConfiguration(ctx context.Context, config *CloudflaredConfig) (*CloudflaredConfig, error)
GetConfiguration(ctx context.Context) (*CloudflaredConfig, error)
}
type CloudflaredServer_PogsClient struct {
Client capnp.Client
Conn *rpc.Conn
}
func (c *CloudflaredServer_PogsClient) Close() error {
return c.Conn.Close()
}
func (c *CloudflaredServer_PogsClient) UseConfiguration(
ctx context.Context,
config *CloudflaredConfig,
) (*UseConfigurationResult, error) {
client := tunnelrpc.CloudflaredServer{Client: c.Client}
promise := client.UseConfiguration(ctx, func(p tunnelrpc.CloudflaredServer_useConfiguration_Params) error {
cloudflaredConfig, err := p.NewCloudflaredConfig()
if err != nil {
return err
}
return MarshalCloudflaredConfig(cloudflaredConfig, config)
})
retval, err := promise.Result().Struct()
if err != nil {
return nil, err
}
return UnmarshalUseConfigurationResult(retval)
}

View File

@@ -0,0 +1,292 @@
package pogs
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/tunnelrpc"
capnp "zombiezen.com/go/capnproto2"
)
func TestCloudflaredConfig(t *testing.T) {
addDoHProxyConfigs := func(c *CloudflaredConfig) {
c.DoHProxyConfigs = []*DoHProxyConfig{
sampleDoHProxyConfig(),
}
}
addReverseProxyConfigs := func(c *CloudflaredConfig) {
c.ReverseProxyConfigs = []*ReverseProxyConfig{
sampleReverseProxyConfig(),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.Origin = sampleHTTPOriginConfig()
}),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.Origin = sampleUnixSocketOriginConfig()
}),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.Origin = sampleWebSocketOriginConfig()
}),
}
}
testCases := []*CloudflaredConfig{
sampleCloudflaredConfig(),
sampleCloudflaredConfig(addDoHProxyConfigs),
sampleCloudflaredConfig(addReverseProxyConfigs),
sampleCloudflaredConfig(addDoHProxyConfigs, addReverseProxyConfigs),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewCloudflaredConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalCloudflaredConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalCloudflaredConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestUseConfigurationResult(t *testing.T) {
testCases := []*UseConfigurationResult{
&UseConfigurationResult{
Success: true,
},
&UseConfigurationResult{
Success: false,
ErrorMessage: "the quick brown fox jumped over the lazy dogs",
},
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewUseConfigurationResult(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalUseConfigurationResult(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalUseConfigurationResult(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestDoHProxyConfig(t *testing.T) {
testCases := []*DoHProxyConfig{
sampleDoHProxyConfig(),
sampleDoHProxyConfig(func(c *DoHProxyConfig) {
c.Upstreams = nil
}),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewDoHProxyConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalDoHProxyConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalDoHProxyConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestReverseProxyConfig(t *testing.T) {
testCases := []*ReverseProxyConfig{
sampleReverseProxyConfig(),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.Origin = sampleHTTPOriginConfig()
}),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.Origin = sampleUnixSocketOriginConfig()
}),
sampleReverseProxyConfig(func(c *ReverseProxyConfig) {
c.Origin = sampleWebSocketOriginConfig()
}),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewReverseProxyConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalReverseProxyConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalReverseProxyConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestHTTPOriginConfig(t *testing.T) {
testCases := []*HTTPOriginConfig{
sampleHTTPOriginConfig(),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewHTTPOriginConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalHTTPOriginConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalHTTPOriginConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestUnixSocketOriginConfig(t *testing.T) {
testCases := []*UnixSocketOriginConfig{
sampleUnixSocketOriginConfig(),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewUnixSocketOriginConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalUnixSocketOriginConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalUnixSocketOriginConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
func TestWebSocketOriginConfig(t *testing.T) {
testCases := []*WebSocketOriginConfig{
sampleWebSocketOriginConfig(),
}
for i, testCase := range testCases {
_, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
capnpEntity, err := tunnelrpc.NewWebSocketOriginConfig(seg)
if !assert.NoError(t, err) {
t.Fatal("Couldn't initialize a new message")
}
err = MarshalWebSocketOriginConfig(capnpEntity, testCase)
if !assert.NoError(t, err, "testCase index %v failed to marshal", i) {
continue
}
result, err := UnmarshalWebSocketOriginConfig(capnpEntity)
if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) {
continue
}
assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i)
}
}
//////////////////////////////////////////////////////////////////////////////
// Functions to generate sample data for ease of testing
func sampleCloudflaredConfig(overrides ...func(*CloudflaredConfig)) *CloudflaredConfig {
// strip the location and monotonic clock reading so that assert.Equals()
// will work correctly
now := time.Now().UTC().Round(0)
sample := &CloudflaredConfig{
Timestamp: now,
AutoUpdateFrequency: 21 * time.Hour,
MetricsUpdateFrequency: 11 * time.Minute,
HeartbeatInterval: 5 * time.Second,
MaxFailedHeartbeats: 9001,
GracePeriod: 31 * time.Second,
}
for _, f := range overrides {
f(sample)
}
return sample
}
func sampleDoHProxyConfig(overrides ...func(*DoHProxyConfig)) *DoHProxyConfig {
sample := &DoHProxyConfig{
ListenHost: "127.0.0.1",
ListenPort: 53,
Upstreams: []string{"https://1.example.com", "https://2.example.com"},
}
for _, f := range overrides {
f(sample)
}
return sample
}
func sampleReverseProxyConfig(overrides ...func(*ReverseProxyConfig)) *ReverseProxyConfig {
sample := &ReverseProxyConfig{
TunnelID: "hijk",
Origin: &HelloWorldOriginConfig{},
Retries: 18,
ConnectionTimeout: 5 * time.Second,
ChunkedEncoding: false,
CompressionQuality: 4,
}
for _, f := range overrides {
f(sample)
}
return sample
}
func sampleHTTPOriginConfig(overrides ...func(*HTTPOriginConfig)) *HTTPOriginConfig {
sample := &HTTPOriginConfig{
URL: "https://example.com",
TCPKeepAlive: 7 * time.Second,
DialDualStack: true,
TLSHandshakeTimeout: 11 * time.Second,
TLSVerify: true,
OriginCAPool: "/etc/cert.pem",
OriginServerName: "secure.example.com",
MaxIdleConnections: 19,
IdleConnectionTimeout: 17 * time.Second,
}
for _, f := range overrides {
f(sample)
}
return sample
}
func sampleUnixSocketOriginConfig(overrides ...func(*UnixSocketOriginConfig)) *UnixSocketOriginConfig {
sample := &UnixSocketOriginConfig{
Path: "/var/lib/file.sock",
}
for _, f := range overrides {
f(sample)
}
return sample
}
func sampleWebSocketOriginConfig(overrides ...func(*WebSocketOriginConfig)) *WebSocketOriginConfig {
sample := &WebSocketOriginConfig{
URL: "ssh://example.com",
}
for _, f := range overrides {
f(sample)
}
return sample
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"zombiezen.com/go/capnproto2"
capnp "zombiezen.com/go/capnproto2"
"zombiezen.com/go/capnproto2/pogs"
"zombiezen.com/go/capnproto2/rpc"
"zombiezen.com/go/capnproto2/server"