From df2e48635b79856497c43ec9d0e6870e1c1c6511 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 28 May 2024 23:30:20 +0000 Subject: [PATCH] Add optional proxy to exchange APIs for light clients This enables light clients to authenticate directly to exchange APIs without revealing their IP address. --- README.md | 4 +- cmd/root.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++ common/common.go | 1 + go.mod | 5 ++- go.sum | 6 +++ 5 files changed, 115 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 215473be..aebf7162 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Lightwalletd uses the following `zcashd` RPCs: ## Lightwalletd -First, install [Go](https://golang.org/dl/#stable) version 1.17 or later. You can see your current version by running `go version`. +First, install [Go](https://golang.org/dl/#stable) version 1.18 or later. You can see your current version by running `go version`. Clone the [current repository](https://github.com/zcash/lightwalletd) into a local directory that is _not_ within any component of your `$GOPATH` (`$HOME/go` by default), then build the lightwalletd server binary by running `make`. @@ -81,7 +81,7 @@ Type `./lightwalletd help` to see the full list of options and arguments. # Production Usage Run a local instance of `zcashd` (see above), except do _not_ specify `--no-tls-very-insecure`. -Ensure [Go](https://golang.org/dl/#stable) version 1.17 or later is installed. +Ensure [Go](https://golang.org/dl/#stable) version 1.18 or later is installed. **x509 Certificates** You will need to supply an x509 certificate that connecting clients will have good reason to trust (hint: do not use a self-signed one, our SDK will reject those unless you distribute them to the client out-of-band). We suggest that you be sure to buy a reputable one from a supplier that uses a modern hashing algorithm (NOT md5 or sha1) and that uses Certificate Transparency (OID 1.3.6.1.4.1.11129.2.4.2 will be present in the certificate). diff --git a/cmd/root.go b/cmd/root.go index b88342e6..dd9e8d89 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,9 @@ package cmd import ( + "context" "fmt" + "io" "net" "net/http" "os" @@ -14,10 +16,14 @@ import ( "github.com/btcsuite/btcd/rpcclient" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "github.com/openconfig/grpctunnel/bidi" + tpb "github.com/openconfig/grpctunnel/proto/tunnel" + "github.com/openconfig/grpctunnel/tunnel" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" + "golang.org/x/exp/maps" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/reflection" @@ -59,6 +65,7 @@ var rootCmd = &cobra.Command{ PingEnable: viper.GetBool("ping-very-insecure"), Darkside: viper.GetBool("darkside-very-insecure"), DarksideTimeout: viper.GetUint64("darkside-timeout"), + ProxyExchangeApis: viper.GetBool("proxy-exchange-apis"), } common.Log.Debugf("Options: %#v\n", opts) @@ -178,6 +185,10 @@ func startServer(opts *common.Options) error { reflection.Register(server) } + if opts.ProxyExchangeApis { + proxyExchangeApis(server) + } + // Initialize Zcash RPC client. Right now (Jan 2018) this is only for // sending transactions, but in the future it could back a different type // of block streamer. @@ -335,6 +346,7 @@ func init() { rootCmd.Flags().Bool("ping-very-insecure", false, "allow Ping GRPC for testing") rootCmd.Flags().Bool("darkside-very-insecure", false, "run with GRPC-controllable mock zcashd for integration testing (shuts down after 30 minutes)") rootCmd.Flags().Int("darkside-timeout", 30, "override 30 minute default darkside timeout") + rootCmd.Flags().Bool("proxy-exchange-apis", false, "allow light clients to query exchange APIs using lightwalletd's IP address") viper.BindPFlag("grpc-bind-addr", rootCmd.Flags().Lookup("grpc-bind-addr")) viper.SetDefault("grpc-bind-addr", "127.0.0.1:9067") @@ -372,6 +384,8 @@ func init() { viper.SetDefault("darkside-very-insecure", false) viper.BindPFlag("darkside-timeout", rootCmd.Flags().Lookup("darkside-timeout")) viper.SetDefault("darkside-timeout", 30) + viper.BindPFlag("proxy-exchange-apis", rootCmd.Flags().Lookup("proxy-exchange-apis")) + viper.SetDefault("proxy-exchange-apis", false) logger.SetFormatter(&logrus.TextFormatter{ //DisableColors: true, @@ -423,3 +437,91 @@ func startHTTPServer(opts *common.Options) { http.Handle("/metrics", promhttp.Handler()) http.ListenAndServe(opts.HTTPBindAddr, nil) } + +func proxyExchangeApis(server *grpc.Server) { + common.Log.Info("Proxying exchange APIs to light clients") + + addTargetHandler := func(t tunnel.Target) error { + common.Log.WithFields(logrus.Fields{"target": t}).Warn("client attempted to register target") + return fmt.Errorf("client targets are not permitted") + } + + // Allowlist of APIs that light clients are permitted to connect to. + // We have one target per API, because the light client needs to know the domain name + // to use for their TLS connection. + apis := make(map[tunnel.Target]string) + apis[tunnel.Target{ID: "binanceApi", Type: "HTTPS"}] = "api.binance.com:443" + apis[tunnel.Target{ID: "coinbaseApi", Type: "HTTPS"}] = "api.exchange.coinbase.com:443" + apis[tunnel.Target{ID: "geminiApi", Type: "HTTPS"}] = "api.gemini.com:443" + + registerHandler := func(ss tunnel.ServerSession) error { + common.Log.WithFields(logrus.Fields{"addr": ss.Addr, "target": ss.Target}).Info("session requested") + for target := range apis { + if ss.Target.ID == target.ID { + return nil + } + } + return fmt.Errorf("target not allowed: %s", ss.Target.ID) + } + + // Handler for a proxy connection. + sessionHandler := func(ss tunnel.ServerSession, rwc io.ReadWriteCloser) error { + common.Log.WithFields(logrus.Fields{"addr": ss.Addr, "target": ss.Target}).Info("new session") + + var dialAddr string + for target, addr := range apis { + if ss.Target.ID == target.ID && ss.Target.Type == target.Type { + dialAddr = addr + break + } + } + if len(dialAddr) == 0 { + return fmt.Errorf("no matching dial port found for target: %s|%s", ss.Target.ID, ss.Target.Type) + } + + // TODO: Enforce per-client (`ss.Addr`) rate limit. + + conn, err := net.Dial("tcp", dialAddr) + if err != nil { + return fmt.Errorf("failed to dial %s: %v", dialAddr, err) + } + + // Proxy traffic between the gRPC connection and the TCP connection. + if err = bidi.Copy(rwc, conn); err != nil { + common.Log.WithFields(logrus.Fields{"error": err}).Info("error while proxying") + } + + common.Log.WithFields(logrus.Fields{"addr": ss.Addr, "target": ss.Target}).Info("session ended") + return nil + } + + var err error + ts, err := tunnel.NewServer(tunnel.ServerConfig{ + AddTargetHandler: addTargetHandler, + RegisterHandler: registerHandler, + Handler: sessionHandler, + LocalTargets: maps.Keys(apis), + }) + if err != nil { + common.Log.WithFields(logrus.Fields{ + "error": err, + }).Fatal("failed to create new gRPC tunnel server") + } + tpb.RegisterTunnelServer(server, ts) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + errChTS := ts.ErrorChan() + go func() { + for { + select { + case err := <-errChTS: + common.Log.WithFields(logrus.Fields{ + "error": err, + }).Errorf("gRPC tunnel server error") + case <-ctx.Done(): + return + } + } + }() +} diff --git a/common/common.go b/common/common.go index 51955e4c..2366829d 100644 --- a/common/common.go +++ b/common/common.go @@ -48,6 +48,7 @@ type Options struct { PingEnable bool `json:"ping_enable"` Darkside bool `json:"darkside"` DarksideTimeout uint64 `json:"darkside_timeout"` + ProxyExchangeApis bool `json:"proxy_exchange_apis,omitempty"` } // RawRequest points to the function to send a an RPC request to zcashd; diff --git a/go.mod b/go.mod index cc412ac2..66677ab5 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,13 @@ module github.com/zcash/lightwalletd -go 1.17 +go 1.18 require ( github.com/btcsuite/btcd v0.24.0 github.com/golang/protobuf v1.5.3 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 + github.com/openconfig/grpctunnel v0.1.0 github.com/prometheus/client_golang v1.18.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 @@ -24,6 +25,7 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect + github.com/cenkalti/backoff/v4 v4.1.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect @@ -33,6 +35,7 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/openconfig/gnmi v0.10.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect diff --git a/go.sum b/go.sum index d666b5a2..bd64d6da 100644 --- a/go.sum +++ b/go.sum @@ -1201,6 +1201,8 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= @@ -1566,6 +1568,10 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/openconfig/gnmi v0.10.0 h1:kQEZ/9ek3Vp2Y5IVuV2L/ba8/77TgjdXg505QXvYmg8= +github.com/openconfig/gnmi v0.10.0/go.mod h1:Y9os75GmSkhHw2wX8sMsxfI7qRGAEcDh8NTa5a8vj6E= +github.com/openconfig/grpctunnel v0.1.0 h1:EN99qtlExZczgQgp5ANnHRC/Rs62cAG+Tz2BQ5m/maM= +github.com/openconfig/grpctunnel v0.1.0/go.mod h1:G04Pdu0pml98tdvXrvLaU+EBo3PxYfI9MYqpvdaEHLo= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=