From db4d325cb6f850e735858a1dd1100f688de37f38 Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 29 Oct 2021 01:20:22 +0200 Subject: [PATCH] node/pkg/p2p: expose network guardian version metric Fixes https://github.com/certusone/wormhole/issues/305 The logic to do this seemingly simple task is hilariously complex due to the version string being attacker-controlled. Change-Id: Ia1758418a67c082595affe0b7f2bb801e9434733 --- node/pkg/p2p/netmetrics.go | 48 +++++++++++++++++++++++++++++++++ node/pkg/p2p/netmetrics_test.go | 33 +++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 node/pkg/p2p/netmetrics_test.go diff --git a/node/pkg/p2p/netmetrics.go b/node/pkg/p2p/netmetrics.go index 7b51f8f05..eb926de54 100644 --- a/node/pkg/p2p/netmetrics.go +++ b/node/pkg/p2p/netmetrics.go @@ -3,10 +3,14 @@ package p2p import ( gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" "github.com/certusone/wormhole/node/pkg/vaa" + "github.com/certusone/wormhole/node/pkg/version" "github.com/ethereum/go-ethereum/common" "github.com/libp2p/go-libp2p-core/peer" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "math" + "regexp" + "strconv" ) var ( @@ -20,6 +24,11 @@ var ( Name: "wormhole_network_node_errors_count", Help: "Number of errors the given guardian node encountered per network", }, []string{"guardian_addr", "node_id", "node_name", "network"}) + wormholeNetworkVersion = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wormhole_network_node_version", + Help: "Network version of the given guardian node per network", + }, []string{"guardian_addr", "node_id", "node_name", "network", "version"}) ) func collectNodeMetrics(addr common.Address, peerId peer.ID, hb *gossipv1.Heartbeat) { @@ -35,5 +44,44 @@ func collectNodeMetrics(addr common.Address, peerId peer.ID, hb *gossipv1.Heartb wormholeNetworkNodeErrors.WithLabelValues( addr.Hex(), peerId.Pretty(), hb.NodeName, chain.String()).Set(float64(n.ErrorCount)) + + wormholeNetworkVersion.WithLabelValues( + addr.Hex(), peerId.Pretty(), hb.NodeName, chain.String(), + sanitizeVersion(hb.Version, version.Version())).Set(1) } } + +var ( + // Parse version string using regular expression. + // The version string should be in the format of "vX.Y.Z" + // where X, Y and Z are integers. Suffixes are ignored. + reVersion = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)`) +) + +// sanitizeVersion cleans up the version string to prevent an attacker from executing a cardinality attack. +func sanitizeVersion(version string, reference string) string { + // Match groups of reVersion + components := reVersion.FindStringSubmatch(version) + referenceComponents := reVersion.FindStringSubmatch(reference) + + // Compare components of the version string with the reference and ensure + // that the distance is less than 5. + for i, c := range components { + if len(referenceComponents) <= i { + return "other" + } + + cInt, _ := strconv.Atoi(c) + cRefInt, _ := strconv.Atoi(referenceComponents[i]) + + if math.Abs(float64(cInt-cRefInt)) > 5 { + return "other" + } + } + + v := reVersion.FindString(version) + if v == "" { + return "other" + } + return v +} diff --git a/node/pkg/p2p/netmetrics_test.go b/node/pkg/p2p/netmetrics_test.go new file mode 100644 index 000000000..4ba6a895b --- /dev/null +++ b/node/pkg/p2p/netmetrics_test.go @@ -0,0 +1,33 @@ +package p2p + +import ( + "testing" +) + +type sanitizeVersionCase struct { + version string + ref string + want string +} + +func Test_sanitizeVersion(t *testing.T) { + cases := []sanitizeVersionCase{ + {version: "v1.0.0", ref: "v1.0.0", want: "v1.0.0"}, + {version: "v1.0.0-foo", ref: "v1.0.0", want: "v1.0.0"}, + {version: "v1.0.0-foo", ref: "v1.0.0-bar", want: "v1.0.0"}, + {version: "v6.0.0-foo", ref: "v1.0.0-bar", want: "v6.0.0"}, + {version: "v6.1.0-foo", ref: "v1.0.0-bar", want: "v6.1.0"}, + {version: "v6.1.0-foo", ref: "v4.5.0-bar", want: "v6.1.0"}, + {version: "v6.1.0.1.1.1", ref: "v4.5.0.2.2.2", want: "v6.1.0"}, + {version: "v10.1.0-foo", ref: "v1.0.0", want: "other"}, + {version: "notaversion", ref: "v1.0.0", want: "other"}, + {version: "v6.1.10000000", ref: "v1.0.0-bar", want: "other"}, + } + + for _, c := range cases { + got := sanitizeVersion(c.version, c.ref) + if got != c.want { + t.Errorf("sanitizeVersion(%q, %q) == %q, want %q", c.version, c.ref, got, c.want) + } + } +}