Skip to content

Commit 6c81d2a

Browse files
guandalivlfig
andauthored
DF-23700 sanitize RPC urls for metrics export (#103)
Co-authored-by: Vasco Figueira <vasco.figueira@smartcontract.com>
1 parent 628f65d commit 6c81d2a

3 files changed

Lines changed: 111 additions & 2 deletions

File tree

metrics/client.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const rpcCallLatencyBeholder = "rpc_call_latency"
3939
type RPCClientMetrics interface {
4040
// RecordRequest records latency for an RPC call (observed in nanoseconds for Prometheus and Beholder).
4141
// Failures use success="false"; derive error rate from rpc_call_latency_count{success="false"} (or equivalent).
42+
// rpcURL is sanitized before export (userinfo and query removed; path hashed).
4243
RecordRequest(ctx context.Context, rpcURL string, isSendOnly bool, callName string, latency time.Duration, err error)
4344
}
4445

@@ -76,13 +77,14 @@ func (m *rpcClientMetrics) RecordRequest(ctx context.Context, rpcURL string, isS
7677
}
7778
sendStr := strconv.FormatBool(isSendOnly)
7879
latencyNs := float64(latency)
80+
safeRPCURL := SanitizeRPCURL(rpcURL)
7981

80-
RPCCallLatency.WithLabelValues(m.chainFamily, m.chainID, rpcURL, sendStr, successStr, callName).Observe(latencyNs)
82+
RPCCallLatency.WithLabelValues(m.chainFamily, m.chainID, safeRPCURL, sendStr, successStr, callName).Observe(latencyNs)
8183

8284
latAttrs := metric.WithAttributes(
8385
attribute.String("chainFamily", m.chainFamily),
8486
attribute.String("chainID", m.chainID),
85-
attribute.String("rpcUrl", rpcURL),
87+
attribute.String("rpcUrl", safeRPCURL),
8688
attribute.String("isSendOnly", sendStr),
8789
attribute.String("success", successStr),
8890
attribute.String("rpcCallName", callName),

metrics/sanitize_rpc_url.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package metrics
2+
3+
import (
4+
"crypto/sha1" //nolint:gosec // sha1 used only for URL anonymisation, not security
5+
"fmt"
6+
"net/url"
7+
"strings"
8+
)
9+
10+
// SanitizeRPCURL either strips user:passwd or replaces path and params with their sha1-hex, excluding leading / if present
11+
func SanitizeRPCURL(raw string) string {
12+
u, err := url.Parse(raw)
13+
if err != nil {
14+
return "invalid_rpc_url"
15+
}
16+
17+
if u.User != nil {
18+
// Strip credentials and leave everything else intact.
19+
u.User = nil
20+
return u.String()
21+
}
22+
23+
// Build the sensitive portion: path (without leading /) plus optional query.
24+
sensitive := strings.TrimPrefix(u.Path, "/")
25+
if u.RawQuery != "" {
26+
if sensitive != "" {
27+
sensitive += "?" + u.RawQuery
28+
} else {
29+
sensitive = u.RawQuery
30+
}
31+
}
32+
33+
if sensitive == "" {
34+
// Nothing to redact.
35+
return u.String()
36+
}
37+
38+
//nolint:gosec
39+
h := sha1.Sum([]byte(sensitive))
40+
u.Path = "/" + fmt.Sprintf("%x", h)
41+
u.RawQuery = ""
42+
return u.String()
43+
}

metrics/sanitize_rpc_url_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package metrics
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestSanitizeRPCURL_RedactsSecrets(t *testing.T) {
10+
cases := []struct {
11+
input string
12+
want string
13+
}{
14+
// simple path
15+
{"https://bsc-mainnet.core.chainstack.com/MjcwMDk3ZGFhMDA5NjJjMDM1", "https://bsc-mainnet.core.chainstack.com/b0b99e8b33b401b05251f91aec08b6a9581c86dd"},
16+
// less simple path
17+
{"http://172.16.156.14:8000/MmZmNTJmOWRiNzg0NTgxNDYyNzJjMTYzNDlmNGJ/iYWEwOTVmYWE0OQ/bsc/mainnet/", "http://172.16.156.14:8000/bd161d616d46b90a248de0f0a3ebc2daf2b8bb20"},
18+
// path with no / is excluded from sha
19+
{"https://anyblocks-01.mainnet.bnb.bdnodes.net?auth=MDcwMTgzODk3NzIyMjU4YzY2MTQzNGMyNTU2OWE2NGEzYjhlODM0NA", "https://anyblocks-01.mainnet.bnb.bdnodes.net/7c87697e63d8f9c049183bb4f8c171af40715b2b"},
20+
// path with leading / is included in sha
21+
{"https://anyblocks-02.mainnet.bnb.bdnodes.net/somepath/?auth=2Dc8bNAqCC0X74zZfi_4ra6XzuBY8lmXcTE1ic9EO5o", "https://anyblocks-02.mainnet.bnb.bdnodes.net/22c028ea2d53fd106e2bb93bc61d838ed6b01c19"},
22+
// strip creds keep path
23+
{"https://myLittleNop:YjY5MjAwOGJkMzBjNW@broadcast-mirror.fiews.io/?chain_id=56", "https://broadcast-mirror.fiews.io/?chain_id=56"},
24+
// even if no creds, sacrifice path for uniformity
25+
{"https://eu-bsc.rpc.linkriver.internal/rpc", "https://eu-bsc.rpc.linkriver.internal/e64b40f2bd5c8a9560773d16476a86ede7e7c1ba"},
26+
// keeps protocol too
27+
{"wss://bsc-mainnet-proxy.internal.linkpool.io/ws", "wss://bsc-mainnet-proxy.internal.linkpool.io/1457b75dc8c5500c0f1d4503cf801b60deb045a4"},
28+
}
29+
30+
for _, tc := range cases {
31+
t.Run(tc.input, func(t *testing.T) {
32+
assert.Equal(t, tc.want, SanitizeRPCURL(tc.input))
33+
})
34+
}
35+
}
36+
37+
func TestSanitizeRPCURL_AlreadySanitized(t *testing.T) {
38+
urls := []string{
39+
"http://10.0.1.191:8545",
40+
"http://144.178.241.22:8545",
41+
"http://222.106.187.14:12001",
42+
"http://at2-bsc-main03.blockchain.fiews.net:8545",
43+
"http://berlioz.stakesystems.io:8745",
44+
"http://blockchains-1.shultzpro.com:8545",
45+
"http://dfw3-bsc-main01.blockchain.fiews.net:8545",
46+
"http://sylvester.stakesystems.io:8745",
47+
"https://bsc-dataseed.binance.org/",
48+
"https://chainlink-bsc.rpc.blxrbdn.com",
49+
"https://puissant-builder.48.club",
50+
"ws://10.0.1.191:8546",
51+
"ws://144.76.108.206:8546",
52+
"ws://172.16.152.140:8546",
53+
"ws://bsc-rpc-2.piertwo.prod:8546",
54+
"ws://bsc.rpc.cinternal.com",
55+
"ws://sylvester.stakesystems.io:8746",
56+
"wss://bsc-rpc.o1.wtf",
57+
}
58+
59+
for _, u := range urls {
60+
t.Run(u, func(t *testing.T) {
61+
assert.Equal(t, u, SanitizeRPCURL(u))
62+
})
63+
}
64+
}

0 commit comments

Comments
 (0)