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
103 changes: 96 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

<details>
<summary><code>/etc/nginx/conf.d/sync_proxy.conf</code></summary>

Expand All @@ -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;
Expand All @@ -73,14 +73,103 @@ server {
proxy_connect_timeout 100ms;
proxy_read_timeout 100ms;
}
}
```

</details>

And if you'd like to use different JWT secrets for different ELs:

<details>
<summary>Example</summary>
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;
}
}
```

</details>

## Caveats
Expand Down
1 change: 1 addition & 0 deletions cmd/jwt-tokens-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
config.json
3 changes: 3 additions & 0 deletions cmd/jwt-tokens-service/README.md
Original file line number Diff line number Diff line change
@@ -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.
73 changes: 73 additions & 0 deletions cmd/jwt-tokens-service/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down