diff --git a/Makefile b/Makefile index 9306beb..f25b586 100644 --- a/Makefile +++ b/Makefile @@ -20,15 +20,10 @@ v: ## Show the version clean: ## Clean the build directory rm -rf build/ -.PHONY: build-cli -build-cli: ## Build the CLI +.PHONY: build +build: ## Build the HTTP server @mkdir -p ./build - go build -trimpath -ldflags "-X github.com/flashbots/orderflow-proxy/common.Version=${VERSION}" -v -o ./build/cli cmd/cli/main.go - -.PHONY: build-httpserver -build-httpserver: ## Build the HTTP server - @mkdir -p ./build - go build -trimpath -ldflags "-X github.com/flashbots/orderflow-proxy/common.Version=${VERSION}" -v -o ./build/httpserver cmd/httpserver/main.go + go build -trimpath -ldflags "-X github.com/flashbots/orderflow-proxy/common.Version=${VERSION}" -v -o ./build/orderflow-proxy cmd/httpserver/main.go ##@ Test & Development @@ -75,17 +70,8 @@ cover-html: ## Run tests with coverage and open the HTML report go tool cover -html=/tmp/go-sim-lb.cover.tmp unlink /tmp/go-sim-lb.cover.tmp -.PHONY: docker-cli -docker-cli: ## Build the CLI Docker image - DOCKER_BUILDKIT=1 docker build \ - --platform linux/amd64 \ - --build-arg VERSION=${VERSION} \ - --file cli.dockerfile \ - --tag your-project \ - . - -.PHONY: docker-httpserver -docker-httpserver: ## Build the HTTP server Docker image +.PHONY: docker +docker: ## Build the HTTP server Docker image DOCKER_BUILDKIT=1 docker build \ --platform linux/amd64 \ --build-arg VERSION=${VERSION} \ diff --git a/README.md b/README.md index 554f361..1f7626f 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,31 @@ -# go-template +# orderflow-proxy [![Goreport status](https://goreportcard.com/badge/github.com/flashbots/orderflow-proxy)](https://goreportcard.com/report/github.com/flashbots/go-template) [![Test status](https://github.com/flashbots/orderflow-proxy/actions/workflows/checks.yml/badge.svg?branch=main)](https://github.com/flashbots/go-template/actions?query=workflow%3A%22Checks%22) -Toolbox and building blocks for new Go projects, to get started quickly and right-footed! - -* [`Makefile`](https://github.com/flashbots/orderflow-proxy/blob/main/Makefile) with `lint`, `test`, `build`, `fmt` and more -* Linting with `gofmt`, `gofumpt`, `go vet`, `staticcheck` and `golangci-lint` -* Logging setup using the [slog logger](https://pkg.go.dev/golang.org/x/exp/slog) (with debug and json logging options) -* [GitHub Workflows](.github/workflows/) for linting and testing, as well as releasing and publishing Docker images -* Entry files for [CLI](/cmd/cli/main.go) and [HTTP server](/cmd/httpserver/main.go) -* Webserver with - * Graceful shutdown, implementing `livez`, `readyz` and draining API handlers - * Prometheus metrics - * Using https://pkg.go.dev/github.com/go-chi/chi/v5 for routing - * [Urfave](https://cli.urfave.org/) for cli args -* https://github.com/uber-go/nilaway -* See also: - * Public project setup: https://github.com/flashbots/flashbots-repository-template - * Repository for common Go utilities: https://github.com/flashbots/go-utils - -Pick and choose whatever is useful to you! Don't feel the need to use everything, or even to follow this structure. - ---- - ## Getting started -**Build CLI** +**Build** ```bash -make build-cli -``` +make build +``** -**Build HTTP server** +## Run -```bash -make build-httpserver -``` +`./build/orderflow-proxy` -**Install dev dependencies** +Will -```bash -go install mvdan.cc/gofumpt@v0.4.0 -go install honnef.co/go/tools/cmd/staticcheck@2024.1.1 -go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3 -go install go.uber.org/nilaway/cmd/nilaway@v0.0.0-20240821220108-c91e71c080b7 -go install github.com/daixiang0/gci@v0.11.2 -``` +* create metrics server +* internal server (liveness endpoints) +* orderflow proxy server that will generate self signed certificate and use it to accept requests that will be proxied to the local builder endpoint -**Lint, test, format** +Flags for the orderflow proxy -```bash -make lint -make test -make fmt +``` +--listen-addr value address to listen on for orderflow proxy API (default: "127.0.0.1:9090") +--builder-endpoint value address to send local ordeflow to (default: "127.0.0.1:8546") +--cert-duration value generated certificate duration (default: 8760h0m0s) +--cert-hosts value [ --cert-hosts value ] generated certificate hosts (default: "127.0.0.1", "localhost") ``` diff --git a/cmd/cli/main.go b/cmd/cli/main.go deleted file mode 100644 index 380a265..0000000 --- a/cmd/cli/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "errors" - "log" - "os" - - "github.com/flashbots/orderflow-proxy/common" - "github.com/urfave/cli/v2" // imports as package "cli" -) - -var flags []cli.Flag = []cli.Flag{ - &cli.BoolFlag{ - Name: "log-json", - Value: false, - Usage: "log in JSON format", - }, - &cli.BoolFlag{ - Name: "log-debug", - Value: false, - Usage: "log debug messages", - }, -} - -func main() { - app := &cli.App{ - Name: "httpserver", - Usage: "Serve API, and metrics", - Flags: flags, - Action: runCli, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} - -func runCli(cCtx *cli.Context) error { - logJSON := cCtx.Bool("log-json") - logDebug := cCtx.Bool("log-debug") - - log := common.SetupLogger(&common.LoggingOpts{ - Debug: logDebug, - JSON: logJSON, - Version: common.Version, - }) - - log.Info("Starting the project") - - log.Debug("debug message") - log.Info("info message") - log.With("key", "value").Warn("warn message") - log.Error("error message", "err", errors.ErrUnsupported) - return nil -} diff --git a/cmd/httpserver/httpserver b/cmd/httpserver/httpserver new file mode 100755 index 0000000..9591f90 Binary files /dev/null and b/cmd/httpserver/httpserver differ diff --git a/cmd/httpserver/main.go b/cmd/httpserver/main.go index af8dd65..d5b552b 100644 --- a/cmd/httpserver/main.go +++ b/cmd/httpserver/main.go @@ -9,15 +9,16 @@ import ( "github.com/flashbots/orderflow-proxy/common" "github.com/flashbots/orderflow-proxy/httpserver" + "github.com/flashbots/orderflow-proxy/proxy" "github.com/google/uuid" "github.com/urfave/cli/v2" // imports as package "cli" ) var flags []cli.Flag = []cli.Flag{ &cli.StringFlag{ - Name: "listen-addr", + Name: "internal-listen-addr", Value: "127.0.0.1:8080", - Usage: "address to listen on for API", + Usage: "address to listen on for internal API", }, &cli.StringFlag{ Name: "metrics-addr", @@ -54,15 +55,35 @@ var flags []cli.Flag = []cli.Flag{ Value: 45, Usage: "seconds to wait in drain HTTP request", }, + &cli.StringFlag{ + Name: "listen-addr", + Value: "127.0.0.1:9090", + Usage: "address to listen on for orderflow proxy API", + }, + &cli.StringFlag{ + Name: "builder-endpoint", + Value: "127.0.0.1:8546", + Usage: "address to send local ordeflow to", + }, + &cli.DurationFlag{ + Name: "cert-duration", + Value: time.Hour * 24 * 365, + Usage: "generated certificate duration", + }, + &cli.StringSliceFlag{ + Name: "cert-hosts", + Value: cli.NewStringSlice("127.0.0.1", "localhost"), + Usage: "generated certificate hosts", + }, } func main() { app := &cli.App{ - Name: "httpserver", + Name: "orderflow-proxy", Usage: "Serve API, and metrics", Flags: flags, Action: func(cCtx *cli.Context) error { - listenAddr := cCtx.String("listen-addr") + internalListenAddr := cCtx.String("internal-listen-addr") metricsAddr := cCtx.String("metrics-addr") logJSON := cCtx.Bool("log-json") logDebug := cCtx.Bool("log-debug") @@ -84,7 +105,7 @@ func main() { } cfg := &httpserver.HTTPServerConfig{ - ListenAddr: listenAddr, + ListenAddr: internalListenAddr, MetricsAddr: metricsAddr, Log: log, EnablePprof: enablePprof, @@ -104,6 +125,31 @@ func main() { exit := make(chan os.Signal, 1) signal.Notify(exit, os.Interrupt, syscall.SIGTERM) srv.RunInBackground() + + builderEndpoint := cCtx.String("builder-endpoint") + listedAddr := cCtx.String("listen-addr") + certDuration := cCtx.Duration("cert-duration") + certHosts := cCtx.StringSlice("cert-hosts") + proxyConfig := &proxy.Config{ + Log: log, + MetricsServer: srv.MetricsSrv, + BuilderEndpoint: builderEndpoint, + ListenAddr: listedAddr, + CertValidDuration: certDuration, + CertHosts: certHosts, + } + + proxy, err := proxy.New(*proxyConfig) + if err != nil { + cfg.Log.Error("failed to create proxy server", "err", err) + return err + } + err = proxy.RunProxyInBackground() + if err != nil { + cfg.Log.Error("failed to start proxy server", "err", err) + return err + } + <-exit // Shutdown server once termination signal is received diff --git a/httpserver/handler.go b/httpserver/handler.go index eda0b86..2817929 100644 --- a/httpserver/handler.go +++ b/httpserver/handler.go @@ -8,7 +8,7 @@ import ( ) func (srv *Server) handleAPI(w http.ResponseWriter, r *http.Request) { - m := srv.metricsSrv.Float64Histogram( + m := srv.MetricsSrv.Float64Histogram( "request_duration_api", "API request handling duration", metrics.UomMicroseconds, diff --git a/httpserver/server.go b/httpserver/server.go index ae4ebec..f196e97 100644 --- a/httpserver/server.go +++ b/httpserver/server.go @@ -33,7 +33,7 @@ type Server struct { log *slog.Logger srv *http.Server - metricsSrv *metrics.MetricsServer + MetricsSrv *metrics.MetricsServer } func New(cfg *HTTPServerConfig) (srv *Server, err error) { @@ -46,7 +46,7 @@ func New(cfg *HTTPServerConfig) (srv *Server, err error) { cfg: cfg, log: cfg.Log, srv: nil, - metricsSrv: metricsSrv, + MetricsSrv: metricsSrv, } srv.isReady.Swap(true) @@ -84,7 +84,7 @@ func (srv *Server) RunInBackground() { if srv.cfg.MetricsAddr != "" { go func() { srv.log.With("metricsAddress", srv.cfg.MetricsAddr).Info("Starting metrics server") - err := srv.metricsSrv.ListenAndServe() + err := srv.MetricsSrv.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { srv.log.Error("HTTP server failed", "err", err) } @@ -115,7 +115,7 @@ func (srv *Server) Shutdown() { ctx, cancel := context.WithTimeout(context.Background(), srv.cfg.GracefulShutdownDuration) defer cancel() - if err := srv.metricsSrv.Shutdown(ctx); err != nil { + if err := srv.MetricsSrv.Shutdown(ctx); err != nil { srv.log.Error("Graceful metrics server shutdown failed", "err", err) } else { srv.log.Info("Metrics server gracefully stopped") diff --git a/proxy/cert_gen.go b/proxy/cert_gen.go new file mode 100644 index 0000000..466c86a --- /dev/null +++ b/proxy/cert_gen.go @@ -0,0 +1,79 @@ +package proxy + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "time" +) + +// hosts is a list of ip / dns names for the certificate +func GenerateCert(validFor time.Duration, hosts []string) (cert, key []byte, err error) { + // copied from https://go.dev/src/crypto/tls/generate_cert.go + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + keyUsage := x509.KeyUsageDigitalSignature + + notBefore := time.Now() + notAfter := notBefore.Add(validFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, nil, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Acme"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: keyUsage, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + // certificate is its own CA + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, nil, err + } + + var certOut bytes.Buffer + if err = pem.Encode(&certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { + return nil, nil, err + } + cert = certOut.Bytes() + + privBytes, err := x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return nil, nil, err + } + + var keyOut bytes.Buffer + err = pem.Encode(&keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) + if err != nil { + return nil, nil, err + } + key = keyOut.Bytes() + return +} diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 0000000..eea57e0 --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,76 @@ +package proxy + +import ( + "bytes" + "crypto/tls" + "errors" + "io" + "log/slog" + "net/http" + "time" + + "github.com/flashbots/orderflow-proxy/metrics" +) + +type Config struct { + Log *slog.Logger + MetricsServer *metrics.MetricsServer + + BuilderEndpoint string + ListenAddr string + CertValidDuration time.Duration + CertHosts []string +} + +type Proxy struct { + Config Config + log *slog.Logger +} + +func New(config Config) (*Proxy, error) { + return &Proxy{ + Config: config, + log: config.Log, + }, nil +} + +func (prx *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + return + } + _, err = http.Post(prx.Config.BuilderEndpoint, "content-type: application/json", bytes.NewBuffer(body)) + w.WriteHeader(http.StatusOK) +} + +func (prx *Proxy) GenerateAndPublish() (tls.Certificate, error) { + cert, key, err := GenerateCert(prx.Config.CertValidDuration, prx.Config.CertHosts) + if err != nil { + return tls.Certificate{}, err + } + // todo: publish + // + return tls.X509KeyPair(cert, key) +} + +func (prx *Proxy) RunProxyInBackground() error { + certificate, err := prx.GenerateAndPublish() + if err != nil { + return err + } + + srv := &http.Server{ + Addr: prx.Config.ListenAddr, + Handler: prx, + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{certificate}, + }, + } + go func() { + prx.log.Info("Starting orderflow proxy", "addr", srv.Addr) + if err := srv.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { + prx.log.Error("Orderflow proxy failed", "err", err) + } + }() + return nil +}