Преглед изворни кода

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
Leo пре 4 година
родитељ
комит
db4d325cb6
2 измењених фајлова са 81 додато и 0 уклоњено
  1. 48 0
      node/pkg/p2p/netmetrics.go
  2. 33 0
      node/pkg/p2p/netmetrics_test.go

+ 48 - 0
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
 }

+ 33 - 0
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)
+		}
+	}
+}