Skip to content
Merged
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
30 changes: 19 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MvProxy

MvProxy is a reverse proxy specifically designed for Tezos nodes, offering a range of features that enhance node performance, security, and manageability.
MvProxy is a reverse proxy specifically designed for Mavryk nodes, offering a range of features that enhance node performance, security, and manageability.

## Features

Expand All @@ -16,9 +16,9 @@ MvProxy is a reverse proxy specifically designed for Tezos nodes, offering a ran

## How to run

Make sure that your Tezos Node is running and set its host address in the `tezos_host` configuration.
Make sure that your Mavryk Node is running and set its host address in the `mavryk_host` configuration.

If you want to test only MvProxy without a real Tezos Node, you can simulate a Tezos Node with our `flextesa.sh` script. Make sure that you have a docker.
If you want to test only MvProxy without a real Mavryk Node, you can simulate a Mavryk Node with our `flextesa.sh` script. Make sure that you have a docker.

```bash
./flextesa.sh
Expand Down Expand Up @@ -84,6 +84,14 @@ cache:
ttl: 5
cors:
enabled: true
allow_headers:
- '*'
allow_origins:
- '*'
allow_methods:
- GET
- POST
- OPTIONS
deny_ips:
enabled: false
values: []
Expand Down Expand Up @@ -124,9 +132,9 @@ rate_limit:
redis:
enabled: false
host: ""
tezos_host:
mavryk_host:
- 127.0.0.1:8732
tezos_host_retry: ""
mavryk_host_retry: ""
```

### Environment Variables
Expand All @@ -135,8 +143,8 @@ You can also configure or overwrite MvProxy with environment variables, using th

- `MVPROXY_DEV_MODE` is a flag to enable dev features like pretty logger.
- `MVPROXY_HOST` is the host of the proxy.
- `MVPROXY_TEZOS_HOST` are the hosts of the tezos nodes.
- `MVPROXY_TEZOS_HOST_RETRY` is the host used when finding a 404 or 410. It's recommended use full or archive nodes.
- `MVPROXY_MAVRYK_HOST` are the hosts of the mavryk nodes.
- `MVPROXY_MAVRYK_HOST_RETRY` is the host used when finding a 404 or 410. It's recommended use full or archive nodes.
- `MVPROXY_REDIS_HOST` is the host of the redis.
- `MVPROXY_REDIS_ENABLE` is a flag to enable redis.
- `MVPROXY_LOAD_BALANCER_TTL` is the time to live to keep using the same node by user IP.
Expand All @@ -151,10 +159,10 @@ You can also configure or overwrite MvProxy with environment variables, using th
- `MVPROXY_RATE_LIMIT_MAX` is the max of requests permitted in a period.
- `MVPROXY_DENY_IPS_ENABLED` is a flag to block IP addresses.
- `MVPROXY_DENY_IPS_VALUES` is the IP Address that will be blocked on the proxy.
- `MVPROXY_DENY_ROUTES_ENABLED` is a flag to block the Tezos node's routes.
- `MVPROXY_DENY_ROUTES_VALUES` is the Tezos nodes routes that will be blocked on the proxy.conf.
- `MVPROXY_ALLOW_ROUTES_ENABLED` is a flag to allow the Tezos node's routes.
- `MVPROXY_ALLOW_ROUTES_VALUES` is the Tezos nodes routes that will be allowed on the proxy.conf.
- `MVPROXY_DENY_ROUTES_ENABLED` is a flag to block the Mavryk node's routes.
- `MVPROXY_DENY_ROUTES_VALUES` is the Mavryk nodes routes that will be blocked on the proxy.conf.
- `MVPROXY_ALLOW_ROUTES_ENABLED` is a flag to allow the Mavryk node's routes.
- `MVPROXY_ALLOW_ROUTES_VALUES` is the Mavryk nodes routes that will be allowed on the proxy.conf.
- `MVPROXY_METRICS_ENABLED` is the flag to enable metrics.
- `MVPROXY_METRICS_PPROF` is the flag to enable pprof.
- `MVPROXY_METRICS_HOST` is the host of the prometheus metrics and pprof (if enabled).
Expand Down
58 changes: 53 additions & 5 deletions balancers/iphash.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
echocache "github.com/fraidev/go-echo-cache"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"log"
)

type ipHashBalancer struct {
Expand All @@ -17,6 +18,7 @@ type ipHashBalancer struct {
random *rand.Rand
store echocache.Cache
TTL int
activeReqs sync.Map // map[string]int, backend address -> active requests
}

func NewIPHashBalancer(targets []*middleware.ProxyTarget, retryTarget *middleware.ProxyTarget, ttl int, store echocache.Cache) middleware.ProxyBalancer {
Expand Down Expand Up @@ -53,28 +55,74 @@ func (b *ipHashBalancer) RemoveTarget(name string) bool {
return false
}

func (b *ipHashBalancer) SetTargets(targets []*middleware.ProxyTarget) {
b.mutex.Lock()
defer b.mutex.Unlock()
b.targets = targets
}

func (b *ipHashBalancer) Next(c echo.Context) *middleware.ProxyTarget {
b.mutex.Lock()
defer b.mutex.Unlock()

if len(b.targets) == 0 {
return nil
} else if len(b.targets) == 1 && b.retryTarget == nil {
log.Printf("[mvproxy] %s -> %s %s", c.RealIP(), b.targets[0].URL.String(), c.Request().URL.Path)
b.incActive(b.targets[0].URL.Host)
return b.targets[0]
}

if c.Get("retry") != nil {
log.Printf("[mvproxy] %s -> %s (retry) %s", c.RealIP(), b.retryTarget.URL.String(), c.Request().URL.Path)
b.incActive(b.retryTarget.URL.Host)
return b.retryTarget
}

ctx := c.Request().Context()
ip := []byte(c.RealIP())
got, err := b.store.Get(ctx, ip)
if err != nil {
i := b.random.Intn(len(b.targets))
b.store.Set(ctx, ip, []byte{byte(i)}, b.TTL)
return b.targets[i]
var target *middleware.ProxyTarget
if err == nil && len(got) > 0 {
backendAddr := string(got)
for _, t := range b.targets {
if t.URL.Host == backendAddr {
target = t
break
}
}
}
if target == nil {
// Least-connections: pick backend with fewest active requests
minReqs := int(^uint(0) >> 1) // max int
for _, t := range b.targets {
v, _ := b.activeReqs.LoadOrStore(t.URL.Host, 0)
if reqs, ok := v.(int); ok && reqs < minReqs {
minReqs = reqs
target = t
}
}
if target == nil {
i := b.random.Intn(len(b.targets))
target = b.targets[i]
}
b.store.Set(ctx, ip, []byte(target.URL.Host), b.TTL)
}

return b.targets[int(got[0])]
b.incActive(target.URL.Host)
log.Printf("[mvproxy] %s -> %s %s", c.RealIP(), target.URL.String(), c.Request().URL.Path)
return target
}

// Call this when a request is done (in a middleware after proxying)
func (b *ipHashBalancer) Done(host string) {
v, _ := b.activeReqs.LoadOrStore(host, 0)
if reqs, ok := v.(int); ok && reqs > 0 {
b.activeReqs.Store(host, reqs-1)
}
}

func (b *ipHashBalancer) incActive(host string) {
v, _ := b.activeReqs.LoadOrStore(host, 0)
b.activeReqs.Store(host, v.(int)+1)
}
13 changes: 6 additions & 7 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ func NewConfig() *Config {

var targets = []*middleware.ProxyTarget{}
var retryTarget *middleware.ProxyTarget = nil
if configFile.TezosHostRetry != "" {
retryTarget = hostToTarget(configFile.TezosHostRetry)
if configFile.MavrykHostRetry != "" {
retryTarget = HostToTarget(configFile.MavrykHostRetry)
}
for _, host := range configFile.TezosHost {
targets = append(targets, hostToTarget(host))
for _, host := range configFile.MavrykHost {
targets = append(targets, HostToTarget(host))
}

var redisClient *redis.Client
Expand All @@ -49,7 +49,7 @@ func NewConfig() *Config {
proxyConfig := middleware.ProxyConfig{
Skipper: middleware.DefaultSkipper,
ContextKey: "target",
RetryCount: len(configFile.TezosHost) + 1,
RetryCount: len(configFile.MavrykHost) + 1,
Balancer: balancer,
RetryFilter: func(c echo.Context, err error) bool {
if httpErr, ok := err.(*echo.HTTPError); ok {
Expand Down Expand Up @@ -171,7 +171,7 @@ func buildLogger(devMode bool) zerolog.Logger {
return log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}

func hostToTarget(host string) *middleware.ProxyTarget {
func HostToTarget(host string) *middleware.ProxyTarget {
hostWithScheme := host
if !strings.Contains(host, "http") {
hostWithScheme = "http://" + host
Expand All @@ -180,6 +180,5 @@ func hostToTarget(host string) *middleware.ProxyTarget {
if err != nil {
log.Fatal().Err(err).Msg("unable to parse host")
}

return &middleware.ProxyTarget{URL: targetURL}
}
7 changes: 5 additions & 2 deletions config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package config
var defaultConfig = &ConfigFile{
DevMode: false,
Host: "0.0.0.0:8080",
TezosHost: []string{"127.0.0.1:8732"},
TezosHostRetry: "",
MavrykHost: []string{"127.0.0.1:8732"},
MavrykHostRetry: "",
Redis: Redis{
Host: "",
Enabled: false,
Expand Down Expand Up @@ -88,5 +88,8 @@ var defaultConfig = &ConfigFile{
},
CORS: CORS{
Enabled: true,
AllowHeaders: []string{"*"},
AllowOrigins: []string{"*"},
AllowMethods: []string{"*"},
},
}
9 changes: 6 additions & 3 deletions config/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ type GC struct {
}

type CORS struct {
Enabled bool `mapstructure:"enabled"`
Enabled bool `mapstructure:"enabled"`
AllowHeaders []string `mapstructure:"allow_headers"`
AllowOrigins []string `mapstructure:"allow_origins"`
AllowMethods []string `mapstructure:"allow_methods"`
}

type GZIP struct {
Expand Down Expand Up @@ -104,6 +107,6 @@ type ConfigFile struct {
CORS CORS `mapstructure:"cors"`
GZIP GZIP `mapstructure:"gzip"`
Host string `mapstructure:"host"`
TezosHost []string `mapstructure:"tezos_host"`
TezosHostRetry string `mapstructure:"tezos_host_retry"`
MavrykHost []string `mapstructure:"mavryk_host"`
MavrykHostRetry string `mapstructure:"mavryk_host_retry"`
}
4 changes: 2 additions & 2 deletions config/viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ func initViper() *ConfigFile {
// Set default values for configuration
viper.SetDefault("dev_mode", false)
viper.SetDefault("host", defaultConfig.Host)
viper.SetDefault("tezos_host", defaultConfig.TezosHost)
viper.SetDefault("tezos_host_retry", defaultConfig.TezosHostRetry)
viper.SetDefault("mavryk_host", defaultConfig.MavrykHost)
viper.SetDefault("mavryk_host_retry", defaultConfig.MavrykHostRetry)
viper.SetDefault("redis.host", defaultConfig.Redis.Host)
viper.SetDefault("redis.enabled", defaultConfig.Redis.Enabled)
viper.SetDefault("load_balancer.ttl", defaultConfig.LoadBalancer.TTL)
Expand Down
43 changes: 39 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@ import (
"context"
"net/http"
"net/http/pprof"
"net"
"strings"
"os"
"os/signal"
"runtime/debug"
"time"
"log"

"github.com/fraidev/echo-contrib/echoprometheus"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/mavryk-network/mvproxy/config"
configpkg "github.com/mavryk-network/mvproxy/config"
"github.com/mavryk-network/mvproxy/middlewares"
"github.com/ziflex/lecho/v3"
)

func main() {
config := config.NewConfig()
config := configpkg.NewConfig()

debug.SetGCPercent(config.ConfigFile.GC.Percent)

Expand All @@ -43,11 +46,43 @@ func main() {
// Start metrics server
startMetricsServer(config)

// Start dynamic DNS resolver for mavryk_host
go func() {
interval := 30 * time.Second
for {
var newTargets []*middleware.ProxyTarget
for _, host := range config.ConfigFile.MavrykHost {
if net.ParseIP(host) == nil && !strings.Contains(host, ":") {
// Host is a DNS name, resolve all A records
ips, err := net.LookupHost(host)
if err == nil {
for _, ip := range ips {
newTargets = append(newTargets, configpkg.HostToTarget(ip+":8732")) // default port, adjust as needed
}
}
} else {
newTargets = append(newTargets, configpkg.HostToTarget(host))
}
}
if len(newTargets) > 0 {
var urls []string
for _, t := range newTargets {
urls = append(urls, t.URL.String())
}
log.Printf("[mvproxy] Updating balancer targets: %v", urls)
if balancer, ok := config.ProxyConfig.Balancer.(interface{ SetTargets([]*middleware.ProxyTarget) }); ok {
balancer.SetTargets(newTargets)
}
}
time.Sleep(interval)
}
}()

// Start proxy
startProxyWithGracefulShutdown(e, config)
}

func startMetricsServer(config *config.Config) {
func startMetricsServer(config *configpkg.Config) {
if config.ConfigFile.Metrics.Enabled {
go func() {
metrics := echo.New()
Expand All @@ -72,7 +107,7 @@ func startMetricsServer(config *config.Config) {

}

func startProxyWithGracefulShutdown(e *echo.Echo, config *config.Config) {
func startProxyWithGracefulShutdown(e *echo.Echo, config *configpkg.Config) {
go func() {
if err := e.Start(config.ConfigFile.Host); err != nil && err != http.ErrServerClosed {
e.Logger.Fatal("Shutting down the server")
Expand Down
6 changes: 3 additions & 3 deletions middlewares/cors.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ func CORS(config *config.Config) echo.MiddlewareFunc {
Skipper: func(c echo.Context) bool {
return !config.ConfigFile.CORS.Enabled
},
AllowHeaders: []string{"*"},
AllowOrigins: []string{"*"},
AllowMethods: []string{"*"},
AllowHeaders: config.ConfigFile.CORS.AllowHeaders,
AllowOrigins: config.ConfigFile.CORS.AllowOrigins,
AllowMethods: config.ConfigFile.CORS.AllowMethods,
})
}
2 changes: 1 addition & 1 deletion middlewares/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var statusCodeRegex = regexp.MustCompile(`code=(\d+)`)
func Retry(config *config.Config) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
if config.ConfigFile.TezosHostRetry == "" ||
if config.ConfigFile.MavrykHostRetry == "" ||
strings.Contains(c.Request().URL.Path, "mempool") ||
strings.Contains(c.Request().URL.Path, "monitor") {
return next(c)
Expand Down
12 changes: 10 additions & 2 deletions mvproxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ cache:
ttl: 5
cors:
enabled: true
allow_headers:
- '*'
allow_origins:
- '*'
allow_methods:
- GET
- POST
- OPTIONS
deny_ips:
enabled: false
values: []
Expand Down Expand Up @@ -76,6 +84,6 @@ rate_limit:
redis:
enabled: false
host: ""
tezos_host:
mavryk_host:
- 127.0.0.1:8732
tezos_host_retry: ""
mavryk_host_retry: ""