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
This commit is contained in:
Leo 2021-10-29 01:20:22 +02:00 committed by Leopold Schabel
parent 621962982a
commit db4d325cb6
2 changed files with 81 additions and 0 deletions

View File

@ -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
}

View File

@ -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)
}
}
}