Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional proxy to exchange APIs for light clients #487

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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).
Expand Down
102 changes: 102 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cmd

import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably use https://pkg.go.dev/golang.org/x/time/rate for this. What we want is:

  • A request rate limiter per exchange, that we configure with their respective public API limits.
  • A session rate limiter per client, that we configure with some low rate that is sufficient for the kinds of requests that honest light clients will be making ("infrequent" queries of market tickers).
    • We can probably initially assume that we have one session per request, but HTTP connection pooling on the light client side may break this assumption.
    • Once we have request-response detection, we can change this to (or add) a request rate limiter per client.
  • Some mechanism for fairly distributing per-exchange request rate limit tokens amongst connected clients (subject to the per-client rate limiter).


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
}
}
}()
}
1 change: 1 addition & 0 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
Loading