Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d641593

Browse files
committed
fix: allow multihost sync to use real h2 (rather than h2c) when using https
1 parent 2faa522 commit d641593

2 files changed

Lines changed: 72 additions & 11 deletions

File tree

docs/src/docs/multihost.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,43 @@ Server manages configuration for all clients:
125125
- **Server permissions**: `Read/Write Config` scoped to `*` + `Receive Shared Repos`
126126
- **Result**: The server can push config changes (repos and plans) to connected clients
127127

128+
## Reverse Proxy
129+
130+
When exposing a Backrest server to remote clients, you only need to expose the sync RPC path. All other Backrest endpoints (UI, admin API, metrics, downloads) should remain on your trusted network.
131+
132+
**Path to expose**: `/v1sync.BackrestSyncService/`
133+
134+
This is the single bidirectional gRPC/Connect stream peers use to sync. The protocol runs its own post-quantum-safe encrypted transport on top of the connection, but you should still terminate TLS at the proxy.
135+
136+
Requirements:
137+
138+
- **HTTP/2 end-to-end** (or h2c to the upstream) — the sync stream is a long-lived bidi stream and will not work over HTTP/1.1.
139+
- **No response buffering** on the proxy.
140+
- **Long timeouts** (hours, not seconds) — the stream is intentionally persistent.
141+
- **No request/response size limits** on the sync path.
142+
143+
### Caddy
144+
145+
```Caddyfile
146+
backrest.example.com {
147+
@sync path /v1sync.BackrestSyncService/*
148+
reverse_proxy @sync h2c://127.0.0.1:9898 {
149+
flush_interval -1
150+
transport http {
151+
read_timeout 24h
152+
write_timeout 24h
153+
}
154+
}
155+
}
156+
```
157+
158+
Replace `127.0.0.1:9898` with your Backrest instance's bind address. Any path other than `/v1sync.BackrestSyncService/*` will return 404, keeping the UI and admin API off the public internet.
159+
160+
If you also want to expose the UI publicly (not recommended without additional auth in front), add a second `reverse_proxy` block without the path matcher — but be aware this also exposes the admin API.
161+
128162
## Troubleshooting
129163

130-
**Client can't connect**: Verify the Instance URL is reachable from the client. The URL should include the port (default 9898). If using a reverse proxy, ensure it supports HTTP/2 (needed for the bidirectional sync stream) and is configured to allow long polling requests (e.g. 10+ minutes). Disable any proxy timeouts or payload size limits that could interfere with the sync connection. Recommend using a modern reverse proxy like Caddy.
164+
**Client can't connect**: Verify the Instance URL is reachable from the client. The URL should include the port (default 9898). If using a reverse proxy, ensure it supports HTTP/2 (needed for the bidirectional sync stream) and is configured to allow long polling requests (e.g. 10+ minutes). Disable any proxy timeouts or payload size limits that could interfere with the sync connection. Recommend using a modern reverse proxy like Caddy. See the [Reverse Proxy](#reverse-proxy) section above for a working Caddy config.
131165

132166
**Pairing fails**: Check that the pairing token hasn't expired and hasn't exceeded its max uses. Generate a new token if needed.
133167

internal/api/syncapi/syncclient.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"fmt"
88
"net"
99
"net/http"
10+
"net/url"
1011
"slices"
12+
"strings"
1113
"sync"
1214
"time"
1315

@@ -37,16 +39,37 @@ type SyncClient struct {
3739
reconnectAttempts int
3840
}
3941

40-
func newInsecureClient() *http.Client {
41-
return &http.Client{
42-
Transport: &http2.Transport{
43-
AllowHTTP: true,
44-
DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
45-
return net.Dial(network, addr)
42+
// newSyncHTTPClient builds the HTTP/2 client used to dial a peer's instance URL.
43+
// http:// URLs use h2c prior knowledge (plaintext HTTP/2). https:// URLs do a
44+
// real TLS handshake and require the peer to negotiate "h2" via ALPN, which
45+
// matches what reverse proxies like Caddy serve on their TLS listeners.
46+
func newSyncHTTPClient(instanceURL string) (*http.Client, error) {
47+
u, err := url.Parse(instanceURL)
48+
if err != nil {
49+
return nil, fmt.Errorf("parse instance URL %q: %w", instanceURL, err)
50+
}
51+
switch strings.ToLower(u.Scheme) {
52+
case "http":
53+
return &http.Client{
54+
Transport: &http2.Transport{
55+
AllowHTTP: true,
56+
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
57+
var d net.Dialer
58+
return d.DialContext(ctx, network, addr)
59+
},
60+
IdleConnTimeout: 300 * time.Second,
61+
ReadIdleTimeout: 60 * time.Second,
4662
},
47-
IdleConnTimeout: 300 * time.Second,
48-
ReadIdleTimeout: 60 * time.Second,
49-
},
63+
}, nil
64+
case "https":
65+
return &http.Client{
66+
Transport: &http2.Transport{
67+
IdleConnTimeout: 300 * time.Second,
68+
ReadIdleTimeout: 60 * time.Second,
69+
},
70+
}, nil
71+
default:
72+
return nil, fmt.Errorf("unsupported instance URL scheme %q in %q (expected http or https)", u.Scheme, instanceURL)
5073
}
5174
}
5275

@@ -60,8 +83,12 @@ func NewSyncClient(
6083
return nil, errors.New("peer instance URL is required")
6184
}
6285

86+
httpClient, err := newSyncHTTPClient(peer.GetInstanceUrl())
87+
if err != nil {
88+
return nil, err
89+
}
6390
client := v1syncconnect.NewBackrestSyncServiceClient(
64-
newInsecureClient(),
91+
httpClient,
6592
peer.GetInstanceUrl(),
6693
)
6794
c := &SyncClient{

0 commit comments

Comments
 (0)