diff --git a/README.md b/README.md index 5460ffe..19a8056 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ Flashbots proxy to allow redundant execution client (EL) state sync post merge. -* Runs a proxy server that proxies requests from a beacon node (BN) to multiple other execution clients -* Can drive EL sync from multiple BNs for redundancy +- Runs a proxy server that proxies requests from a beacon node (BN) to multiple other execution clients +- Can drive EL sync from multiple BNs for redundancy ## Getting Started -* Run a BN with the execution endpoint pointing to the proxy (default is `localhost:25590`). -* Start the proxy with a flag specifying one or multiple EL endpoints (make sure to point to the authenticated port). +- Run a BN with the execution endpoint pointing to the proxy (default is `localhost:25590`). +- Start the proxy with a flag specifying one or multiple EL endpoints (make sure to point to the authenticated port). ```bash git clone https://github.com/flashbots/sync-proxy.git @@ -35,6 +35,7 @@ The sync proxy can also be used with nginx, with requests proxied from the beaco ![nginx setup overview](docs/nginx-setup.png) An example nginx config like this can be run with the sync proxy: +
/etc/nginx/conf.d/sync_proxy.conf @@ -48,7 +49,6 @@ server { location / { mirror /sync_proxy_1; mirror /sync_proxy_2; - mirror /sync_proxy_3; proxy_pass http://localhost:8551; proxy_set_header X-Real-IP $remote_addr; @@ -73,14 +73,103 @@ server { proxy_connect_timeout 100ms; proxy_read_timeout 100ms; } +} +``` + +
+ +And if you'd like to use different JWT secrets for different ELs: + +
+Example +First, install jwt-tokens-service: `go install github.com/flashbots/sync-proxy/cmd/jwt-tokens-service@latest` + +Set up the service, e.g. for systemd: + +``` +[Unit] +Description=JWT tokens service +After=network.target +Wants=network.target + +[Service] +Type=simple + +ExecStart=/.../jwt-tokens-service \ + -config /.../jwt-secrets.json \ + -client-id some-cl-name + +[Install] +WantedBy=default.target +``` - location = /sync_proxy_3 { +Generate a secret for each EL with `openssl rand -hex 32` and put them in a JSON file: + +```json +{ + "sync-proxy-1": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee", + "sync-proxy-2": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +} +``` + +Then, set up nginx: + +``` +# /etc/nginx/conf.d/sync_proxy.conf +server { + listen 8552; + listen [::]:8552; + + server_name _; + + location / { + mirror /sync_proxy_1; + mirror /sync_proxy_2; + + auth_request /_tokens; + # make sure to lowercase and replace dashes with underscores from names in json config + auth_request_set $auth_header_sync_proxy_1 $upstream_http_authorization_sync_proxy_1; + auth_request_set $auth_header_sync_proxy_2 $upstream_http_authorization_sync_proxy_2; + + proxy_pass http://localhost:8551; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + proxy_set_header Referer $http_referer; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location = /_tokens { internal; - proxy_pass http://sync-proxy-3.local:8552$request_uri; + proxy_pass http://127.0.0.1:1337/tokens/; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + } + + # + # execution nodes + # + location = /sync_proxy_1 { + internal; + proxy_pass http://sync-proxy-1.local:8552$request_uri; proxy_connect_timeout 100ms; proxy_read_timeout 100ms; + + proxy_hide_header Authorization; + proxy_set_header Authorization $auth_header_sync_proxy_1; } + + location = /sync_proxy_2 { + internal; + proxy_pass http://sync-proxy-2.local:8552$request_uri; + proxy_connect_timeout 100ms; + proxy_read_timeout 100ms; + + proxy_hide_header Authorization; + proxy_set_header Authorization $auth_header_sync_proxy_2; + } +} ``` +
## Caveats diff --git a/cmd/jwt-tokens-service/.gitignore b/cmd/jwt-tokens-service/.gitignore new file mode 100644 index 0000000..d344ba6 --- /dev/null +++ b/cmd/jwt-tokens-service/.gitignore @@ -0,0 +1 @@ +config.json diff --git a/cmd/jwt-tokens-service/README.md b/cmd/jwt-tokens-service/README.md new file mode 100644 index 0000000..7f6d5ca --- /dev/null +++ b/cmd/jwt-tokens-service/README.md @@ -0,0 +1,3 @@ +# jwt-tokens-service + +Small service to generate JWT tokens for multiple EL hosts. Intended to be used with nginx auth_request module, see root readme for example. diff --git a/cmd/jwt-tokens-service/main.go b/cmd/jwt-tokens-service/main.go new file mode 100644 index 0000000..049cbd3 --- /dev/null +++ b/cmd/jwt-tokens-service/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "os" + + "github.com/golang-jwt/jwt" +) + +var ( + listenAddr = flag.String("addr", "localhost:1337", "listen address") + configFile = flag.String("config", "config.json", "path to the config file") + clientID = flag.String("client-id", "", "CL client id, optional") +) + +func main() { + flag.Parse() + + f, err := os.Open(*configFile) + if err != nil { + log.Fatalf("failed to open config file: %v", err) + } + defer f.Close() + + // host name => hex jwt secret + var secrets map[string]string + if err := json.NewDecoder(f).Decode(&secrets); err != nil { + log.Fatalf("failed to read config file: %v", err) + } + + http.HandleFunc("/tokens/", func(w http.ResponseWriter, r *http.Request) { + log.Printf("requested tokens from %s", r.RemoteAddr) + + for host, secret := range secrets { + token, err := generateJWT(secret) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to generate token for %s: %v", host, err), http.StatusInternalServerError) + return + } + + w.Header().Set("Authorization-"+host, "Bearer "+token) + } + + w.WriteHeader(http.StatusOK) + }) + + log.Printf("Starting server on %s", *listenAddr) + if err := http.ListenAndServe(*listenAddr, nil); err != nil { + log.Fatalf("Server failed: %v", err) + } +} + +func generateJWT(secretHex string) (string, error) { + secret, err := hex.DecodeString(secretHex) + if err != nil { + return "", fmt.Errorf("invalid hex secret: %v", err) + } + + token := jwt.New(jwt.SigningMethodHS256) + claims := token.Claims.(jwt.MapClaims) + + claims["iat"] = jwt.TimeFunc().Unix() + if *clientID != "" { + claims["id"] = *clientID + } + + return token.SignedString(secret) +} diff --git a/go.mod b/go.mod index f793c3a..cf57031 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( require ( github.com/ethereum/go-ethereum v1.15.2 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/gorilla/mux v1.8.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index a6f4400..d270d35 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=