diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml index bc6622e..55b18c6 100644 --- a/.github/workflows/build-release.yaml +++ b/.github/workflows/build-release.yaml @@ -21,12 +21,12 @@ jobs: name: Building, ${{ matrix.TARGET }} steps: - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.20.12 + go-version: 1.24.1 - name: Update sources if: matrix.TARGET == 'linux/aarch64' || matrix.TARGET == 'windows/x64' diff --git a/README.md b/README.md index c2368c1..6561ecb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # DNSSeeder -**This project is currently under active development and is in Beta -state.** - [![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](https://choosealicense.com/licenses/isc/) DNSSeeder exposes a list of known peers to any new peer joining the @@ -23,8 +20,6 @@ of a random selection of the reliable nodes it knows about. It is written in Go (golang). -This project is currently under active development and is in Beta state. - ## Requirements Latest version of [Go](http://golang.org) diff --git a/checkversion/checkversion.go b/checkversion/checkversion.go new file mode 100644 index 0000000..2960d94 --- /dev/null +++ b/checkversion/checkversion.go @@ -0,0 +1,39 @@ +package checkversion + +import ( + "github.com/hashicorp/go-version" + "github.com/pkg/errors" + _ "net/http/pprof" + "regexp" + "strings" +) + +var versionRegexp = regexp.MustCompile(`\b(\d+\.\d+\.\d+)\b`) + +func CheckVersion(minUaVer string, userAgent string) error { + if minUaVer == "" { + return nil + } + minVer, err := version.NewVersion(minUaVer) + if err != nil { + return err + } + trimmed := strings.TrimPrefix(userAgent, "/") + end := strings.Index(trimmed, "/") + if end == -1 { + return errors.New("Invalid userAgent format") + } + firstGroup := trimmed[:end] + matches := versionRegexp.FindStringSubmatch(firstGroup) + if len(matches) < 2 { + return errors.New("No valid version found in userAgent") + } + clientVer, err := version.NewVersion(matches[1]) + if err != nil { + return err + } + if clientVer.LessThan(minVer) { + return errors.New("UserAgent version is below minimum required") + } + return nil +} diff --git a/checkversion/checkversion_test.go b/checkversion/checkversion_test.go new file mode 100644 index 0000000..5d32ded --- /dev/null +++ b/checkversion/checkversion_test.go @@ -0,0 +1,41 @@ +package checkversion + +import ( + "testing" +) + +func TestCheckVersion(t *testing.T) { + tests := []struct { + minUaVer string + userAgent string + shouldFail bool + }{ + {"0.3.14", "/spectred:0.3.14/spectred:0.3.14/", false}, + {"0.3.14", "/spectred:0.3.14/spectred:0.0.0/", false}, + {"0.3.14", "/spectred:0.3.14/spectred:0.3.14(spectre-desktop_0.3.14)/", false}, + {"0.3.14", "/spectred:0.3.19/spectred:0.3.19/", false}, + {"0.3.14", "/spectred:1.1.0/", false}, + + {"0.3.57", "/spectred:0.3.14/spectred:0.3.14/", true}, + {"0.3.57", "/spectred:0.3.14/spectred:0.0.0/", true}, + {"0.3.57", "/spectred:0.3.14/spectred:0.3.14(spectre-desktop_0.3.14)/", true}, + {"0.3.57", "/spectred:0.3.57/spectred:0.3.57/", false}, + {"0.3.57", "/spectred:1.1.0/", false}, + + {"1.0.0", "/spectred:0.3.14/spectred:0.3.14/", true}, + {"1.0.0", "/spectred:0.3.14/spectred:0.0.0/", true}, + {"1.0.0", "/spectred:0.3.14/spectred:0.3.14(spectre-desktop_0.3.14)/", true}, + {"1.0.0", "/spectred:0.18.9/spectred:0.18.9/", true}, + {"1.0.0", "/spectred:1.1.0/", false}, + } + + for _, tt := range tests { + err := CheckVersion(tt.minUaVer, tt.userAgent) + if tt.shouldFail && err == nil { + t.Errorf("Expected failure for %q with %q, but got nil", tt.minUaVer, tt.userAgent) + } + if !tt.shouldFail && err != nil { + t.Errorf("Unexpected error for %q with %q: %v", tt.minUaVer, tt.userAgent, err) + } + } +} diff --git a/config.go b/config.go index 0ad82c0..24bf043 100644 --- a/config.go +++ b/config.go @@ -14,8 +14,8 @@ import ( "github.com/spectre-project/spectred/infrastructure/config" - "github.com/spectre-project/dnsseeder/version" "github.com/pkg/errors" + "github.com/spectre-project/dnsseeder/version" "github.com/jessevdk/go-flags" "github.com/spectre-project/spectred/util" @@ -28,6 +28,7 @@ const ( defaultListenPort = "5354" defaultGrpcListenPort = "3737" defaultLogLevel = "info" + defaultThreads = 8 ) var ( @@ -55,8 +56,12 @@ type ConfigFlags struct { Seeder string `short:"s" long:"default-seeder" description:"IP address of a working node, optionally with a port specifier"` Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` GRPCListen string `long:"grpclisten" description:"Listen gRPC requests on address:port"` + MinProtoVer uint8 `short:"v" long:"minprotocolversion" description:"Minimum p2p protocol version for nodes."` + MinUaVer string `long:"minuseragentversion" description:"Minimum user agent version for nodes. Prefer minprotocolversion if possible."` + NetSuffix uint16 `long:"netsuffix" description:"Testnet network suffix number"` NoLogFiles bool `long:"nologfiles" description:"Disable logging to file"` LogLevel string `long:"loglevel" description:"Loglevel for stdout (console). Default: info"` + Threads uint8 `long:"threads" description:"Number of threads to use for polling."` config.NetworkFlags } @@ -104,6 +109,7 @@ func loadConfig() (*ConfigFlags, error) { Listen: normalizeAddress("localhost", defaultListenPort), GRPCListen: normalizeAddress("localhost", defaultGrpcListenPort), LogLevel: defaultLogLevel, + Threads: defaultThreads, } preCfg := activeConfig @@ -171,6 +177,23 @@ func loadConfig() (*ConfigFlags, error) { return nil, err } + if activeConfig.NetSuffix != 0 { + if !activeConfig.Testnet { + return nil, errors.New("Net suffix can only be used with testnet") + } + + switch activeConfig.NetSuffix { + case 8: + activeConfig.NetParams().DefaultPort = "18211" + activeConfig.NetParams().Name = "spectre-testnet-8" + case 11: + activeConfig.NetParams().DefaultPort = "18311" + activeConfig.NetParams().Name = "spectre-testnet-11" + default: + return nil, errors.New("Only explicit testnet suffixes are 8 and 11") + } + } + activeConfig.AppDir = cleanAndExpandPath(activeConfig.AppDir) // Append the network type to the app directory so it is "namespaced" // per network. @@ -196,6 +219,13 @@ func loadConfig() (*ConfigFlags, error) { initLog(activeConfig.NoLogFiles, activeConfig.LogLevel, appLogFile, appErrLogFile) + if activeConfig.Threads < 1 || activeConfig.Threads > 32 { + str := "threads must be between 1 and 32" + err := errors.Errorf(str) + fmt.Fprintln(os.Stderr, err) + return nil, err + } + return activeConfig, nil } diff --git a/dns.go b/dns.go index 4309c67..4c952d1 100644 --- a/dns.go +++ b/dns.go @@ -16,8 +16,8 @@ import ( "github.com/spectre-project/spectred/domain/consensus/model/externalapi" "github.com/spectre-project/spectred/domain/consensus/utils/subnetworks" - "github.com/spectre-project/spectred/infrastructure/network/dnsseed" "github.com/pkg/errors" + "github.com/spectre-project/spectred/infrastructure/network/dnsseed" "github.com/miekg/dns" ) @@ -141,7 +141,7 @@ func (d *DNSServer) validateDNSRequest(addr *net.UDPAddr, b []byte) (dnsMsg *dns domainName = strings.ToLower(dnsMsg.Question[0].Name) ff := strings.LastIndex(domainName, d.hostname) if ff < 0 { - str := fmt.Sprintf("invalid name: %s", dnsMsg.Question[0].Name) + str := fmt.Sprintf("%s: invalid name: %s", addr, dnsMsg.Question[0].Name) log.Infof("%s", str) return nil, "", "", errors.Errorf("%s", str) } diff --git a/dnsseed.go b/dnsseed.go index dc65cd9..7938383 100644 --- a/dnsseed.go +++ b/dnsseed.go @@ -6,6 +6,7 @@ package main import ( "fmt" + "github.com/spectre-project/dnsseeder/checkversion" "net" "os" "strconv" @@ -14,9 +15,9 @@ import ( "sync/atomic" "time" + "github.com/spectre-project/dnsseeder/netadapter" "github.com/spectre-project/spectred/app/protocol/common" "github.com/spectre-project/spectred/infrastructure/config" - "github.com/spectre-project/spectred/infrastructure/network/netadapter/standalone" "github.com/pkg/errors" @@ -53,9 +54,9 @@ func hostLookup(host string) ([]net.IP, error) { func creep() { defer wg.Done() - netAdapter, err := standalone.NewMinimalNetAdapter(&config.Config{Flags: &config.Flags{NetworkFlags: ActiveConfig().NetworkFlags}}) - if err != nil { - panic(errors.Wrap(err, "Could not start net adapter")) + var netAdapters []*netadapter.DnsseedNetAdapter + for i := uint8(0); i < ActiveConfig().Threads; i++ { + netAdapters = append(netAdapters, newNetAdapter()) } var knownPeers []*appmessage.NetAddress @@ -84,8 +85,8 @@ func creep() { amgr.AddAddresses(knownPeers) for _, peer := range knownPeers { - amgr.Good(peer.IP, nil) - amgr.Attempt(peer.IP) + amgr.Attempt(peer) + amgr.Good(peer, nil, nil) } } @@ -101,8 +102,8 @@ func creep() { peers = amgr.Addresses() } if len(peers) == 0 { - log.Infof("No stale addresses -- sleeping for 10 minutes") - for i := 0; i < 600; i++ { + log.Infof("No stale addresses -- sleeping for 1 minute") + for i := 0; i < 60; i++ { time.Sleep(time.Second) if atomic.LoadInt32(&systemShutdown) != 0 { log.Infof("Creep thread shutdown") @@ -112,7 +113,7 @@ func creep() { continue } - for _, addr := range peers { + for i, addr := range peers { if atomic.LoadInt32(&systemShutdown) != 0 { log.Infof("Waiting creep threads to terminate") wgCreep.Wait() @@ -120,10 +121,11 @@ func creep() { return } wgCreep.Add(1) + i := i go func(addr *appmessage.NetAddress) { defer wgCreep.Done() - err := pollPeer(netAdapter, addr) + err := pollPeer(netAdapters[i%len(netAdapters)], addr) if err != nil { log.Debugf(err.Error()) if defaultSeeder != nil && addr == defaultSeeder { @@ -136,16 +138,24 @@ func creep() { } } -func pollPeer(netAdapter *standalone.MinimalNetAdapter, addr *appmessage.NetAddress) error { - defer amgr.Attempt(addr.IP) +func pollPeer(netAdapter *netadapter.DnsseedNetAdapter, addr *appmessage.NetAddress) error { + amgr.Attempt(addr) peerAddress := net.JoinHostPort(addr.IP.String(), strconv.Itoa(int(addr.Port))) - routes, err := netAdapter.Connect(peerAddress) + + log.Debugf("Polling peer %s", peerAddress) + routes, msgVersion, err := netAdapter.Connect(peerAddress) if err != nil { return errors.Wrapf(err, "could not connect to %s", peerAddress) } defer routes.Disconnect() + // Abort before collecting peers for nodes below minimum protocol + if ActiveConfig().MinProtoVer > 0 && msgVersion.ProtocolVersion < uint32(ActiveConfig().MinProtoVer) { + return errors.Errorf("Peer %s (%s) protocol version %d is below minimum: %d", + peerAddress, msgVersion.UserAgent, msgVersion.ProtocolVersion, ActiveConfig().MinProtoVer) + } + msgRequestAddresses := appmessage.NewMsgRequestAddresses(true, nil) err = routes.OutgoingRoute.Enqueue(msgRequestAddresses) if err != nil { @@ -159,14 +169,29 @@ func pollPeer(netAdapter *standalone.MinimalNetAdapter, addr *appmessage.NetAddr msgAddresses := message.(*appmessage.MsgAddresses) added := amgr.AddAddresses(msgAddresses.AddressList) - log.Infof("Peer %s sent %d addresses, %d new", - peerAddress, len(msgAddresses.AddressList), added) - - amgr.Good(addr.IP, nil) - + log.Infof("Peer %s (%s) sent %d addresses, %d new", + peerAddress, msgVersion.UserAgent, len(msgAddresses.AddressList), added) + + // Abort after collecting peers for nodes below minimum user agent version + if ActiveConfig().MinUaVer != "" { + err = checkversion.CheckVersion(ActiveConfig().MinUaVer, msgVersion.UserAgent) + if err != nil { + return errors.Wrapf(err, "Peer %s version %s doesn't satisfy minimum: %s", + peerAddress, msgVersion.UserAgent, ActiveConfig().MinUaVer) + } + } + amgr.Good(addr, &msgVersion.UserAgent, nil) return nil } +func newNetAdapter() *netadapter.DnsseedNetAdapter { + netAdapter, err := netadapter.NewDnsseedNetAdapter(&config.Config{Flags: &config.Flags{NetworkFlags: ActiveConfig().NetworkFlags}}) + if err != nil { + panic(errors.Wrap(err, "Could not start net adapter")) + } + return netAdapter +} + func main() { defer panics.HandlePanic(log, "main", nil) interrupt := signal.InterruptListener() @@ -200,13 +225,13 @@ func main() { if len(cfg.Seeder) != 0 { // Prepare the seeder address, supporting either a simple IP with default network port // or a full IP:port format - seederIp := cfg.Seeder + seederIP := cfg.Seeder seederPort := peersDefaultPort // Try to split seeder host and port - foundIp, foundPort, err := net.SplitHostPort(cfg.Seeder) - if (err == nil) { - seederIp = foundIp + foundIP, foundPort, err := net.SplitHostPort(cfg.Seeder) + if err == nil { + seederIP = foundIP seederPort, err = strconv.Atoi(foundPort) if err != nil { log.Errorf("Invalid seeder port: %s", foundPort) @@ -214,15 +239,15 @@ func main() { } } - ip := net.ParseIP(seederIp) + ip := net.ParseIP(seederIP) if ip == nil { - hostAddrs, err := net.LookupHost(seederIp) + hostAddrs, err := net.LookupHost(seederIP) if err != nil { - log.Warnf("Failed to resolve seed host: %v, %v, ignoring", seederIp, err) + log.Warnf("Failed to resolve seed host: %v, %v, ignoring", seederIP, err) } else { ip = net.ParseIP(hostAddrs[0]) if ip == nil { - log.Warnf("Failed to resolve seed host: %v, ignoring", seederIp) + log.Warnf("Failed to resolve seed host: %v, ignoring", seederIP) } } } diff --git a/docker/Dockerfile b/docker/Dockerfile index 4becfb7..17d459b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # -- multistage docker build: stage #1: build stage -FROM golang:1.18-alpine AS build +FROM golang:1.24.1-alpine AS build RUN mkdir -p /go/src/github.com/spectre-project/dnsseeder diff --git a/go.mod b/go.mod index e01d01e..b8e8e76 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,36 @@ module github.com/spectre-project/dnsseeder -go 1.19 +go 1.23.0 + +toolchain go1.24.1 require ( - github.com/jessevdk/go-flags v1.5.0 - github.com/miekg/dns v1.1.59 + github.com/hashicorp/go-version v1.7.0 + github.com/jessevdk/go-flags v1.6.1 + github.com/miekg/dns v1.1.65 github.com/pkg/errors v0.9.1 - github.com/spectre-project/spectred v0.0.0-20240426145300-c7e56aa46707 - google.golang.org/grpc v1.63.2 + github.com/spectre-project/spectred v0.3.15 + google.golang.org/grpc v1.72.0 ) require ( github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect + github.com/cespare/xxhash v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/jrick/logrotate v1.0.0 // indirect + github.com/dchest/siphash v1.2.3 // indirect + github.com/jrick/logrotate v1.1.2 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/segmentio/fasthash v1.0.3 // indirect github.com/spectre-project/go-muhash v0.0.1 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/mod v0.16.0 // indirect - golang.org/x/net v0.22.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.19.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/protobuf v1.33.0 // indirect + github.com/spectre-project/go-spectrex v0.0.0-20240618094337-b3560f5cab2d // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/tools v0.32.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 31af103..b43d538 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,129 @@ +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= -github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= -github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= -github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/jrick/logrotate v1.1.2 h1:6ePk462NCX7TfKtNp5JJ7MbA2YIslkpfgP03TlTYMN0= +github.com/jrick/logrotate v1.1.2/go.mod h1:f9tdWggSVK3iqavGpyvegq5IhNois7KXmasU6/N96OQ= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= +github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= +github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spectre-project/go-muhash v0.0.1 h1:UxikIJ+NL/9TVqvqexVBoQkkrnmTzSeiJocMA48R764= github.com/spectre-project/go-muhash v0.0.1/go.mod h1:KprvUX7HcxTrhCY21bSRyufUKB2LRdX3syPOpVAbSJI= -github.com/spectre-project/spectred v0.0.0-20240426145300-c7e56aa46707 h1:srGHXc+SWMj84CblfiNscOafGmfYok26GahxC8vMa6c= -github.com/spectre-project/spectred v0.0.0-20240426145300-c7e56aa46707/go.mod h1:WOz9Km3kBKsJ8XtQLyOc/UDjlbyM3/s5HqRD+kZzUM0= +github.com/spectre-project/go-secp256k1 v0.0.1 h1:dWznplP1UtUFajy9HGqzywb7v7mHHZ4WVEZMwoqJ0+s= +github.com/spectre-project/go-secp256k1 v0.0.1/go.mod h1:fuNi550cMAuNz6YnhmnH0zJ8Ro6OZiKhpItEUWE7HCQ= +github.com/spectre-project/go-spectrex v0.0.0-20240618094337-b3560f5cab2d h1:rYOKf47/GaQHMrbLc9wHt+8HYlGpHUWgM2jzN1t4gQM= +github.com/spectre-project/go-spectrex v0.0.0-20240618094337-b3560f5cab2d/go.mod h1:dL3/Y9B0Dzift+rpCFIDqbqhM4ZZRtFdMhf1JQ2whrk= +github.com/spectre-project/spectred v0.3.15 h1:S9xKUwjG2nZZ7+t6+2vyVblh+Unx/v1yv6H2C8gZ6KY= +github.com/spectre-project/spectred v0.3.15/go.mod h1:MtENMIOJR0qvj5vFDQ4nUeSjSiN/gtnTF1cP0RQrzVw= +github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs= +github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= -golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= diff --git a/grpc.go b/grpc.go index 2f7dc8d..1844abb 100644 --- a/grpc.go +++ b/grpc.go @@ -8,10 +8,10 @@ import ( "github.com/spectre-project/spectred/domain/consensus/model/externalapi" "github.com/spectre-project/spectred/domain/consensus/utils/subnetworks" - "github.com/spectre-project/spectred/app/appmessage" - "github.com/spectre-project/spectred/infrastructure/network/dnsseed/pb" "github.com/miekg/dns" "github.com/pkg/errors" + "github.com/spectre-project/spectred/app/appmessage" + "github.com/spectre-project/spectred/infrastructure/network/dnsseed/pb" "google.golang.org/grpc" ) diff --git a/manager.go b/manager.go index 9e2c829..601f293 100644 --- a/manager.go +++ b/manager.go @@ -6,23 +6,24 @@ package main import ( "encoding/json" - "net" "os" "path/filepath" + "strconv" "sync" "time" "github.com/spectre-project/spectred/infrastructure/network/addressmanager" - "github.com/spectre-project/spectred/app/appmessage" - "github.com/spectre-project/spectred/domain/consensus/model/externalapi" "github.com/miekg/dns" "github.com/pkg/errors" + "github.com/spectre-project/spectred/app/appmessage" + "github.com/spectre-project/spectred/domain/consensus/model/externalapi" ) // Node repesents a node in the Spectre network type Node struct { Addr *appmessage.NetAddress + UserAgent *string LastAttempt time.Time LastSuccess time.Time LastSeen time.Time @@ -44,13 +45,17 @@ const ( // defaultMaxAddresses is the maximum number of addresses to return. defaultMaxAddresses = 16 - // defaultStaleTimeout is the time in which a host is considered - // stale. - defaultStaleTimeout = time.Hour + // defaultStaleGoodTimeout is the time in which a previously reachable + // node is considered stale. + defaultStaleGoodTimeout = time.Hour + + // defaultStaleBadTimeout is the time in which a previously unreachable + // node is considered stale. + defaultStaleBadTimeout = time.Hour * 2 // dumpAddressInterval is the interval used to dump the address // cache to disk for future use. - dumpAddressInterval = time.Second * 30 + dumpAddressInterval = time.Minute * 2 // peersFilename is the name of the file. peersFilename = "nodes.json" @@ -96,10 +101,10 @@ func (m *Manager) AddAddresses(addrs []*appmessage.NetAddress) int { m.mtx.Lock() for _, addr := range addrs { - if !addressmanager.IsRoutable(addr, ActiveConfig().NetParams().AcceptUnroutable) { + if addr.Port == 0 || !addressmanager.IsRoutable(addr, ActiveConfig().NetParams().AcceptUnroutable) { continue } - addrStr := addr.IP.String() + addrStr := addr.IP.String() + "_" + strconv.Itoa(int(addr.Port)) _, exists := m.nodes[addrStr] if exists { @@ -120,17 +125,15 @@ func (m *Manager) AddAddresses(addrs []*appmessage.NetAddress) int { // Addresses returns IPs that need to be tested again. func (m *Manager) Addresses() []*appmessage.NetAddress { - addrs := make([]*appmessage.NetAddress, 0, defaultMaxAddresses*8) - now := time.Now() - i := defaultMaxAddresses + addrs := make([]*appmessage.NetAddress, 0, 2000) + i := ActiveConfig().Threads * 3 m.mtx.RLock() for _, node := range m.nodes { if i == 0 { break } - if now.Sub(node.LastSuccess) < defaultStaleTimeout || - now.Sub(node.LastAttempt) < defaultStaleTimeout { + if !isStale(node) { continue } addrs = append(addrs, node.Addr) @@ -157,32 +160,21 @@ func (m *Manager) GoodAddresses(qtype uint16, includeAllSubnetworks bool, subnet return addrs } - now := time.Now() m.mtx.RLock() for _, node := range m.nodes { if i == 0 { break } - - if node.Addr.Port != uint16(peersDefaultPort) { - continue - } - if !includeAllSubnetworks && !node.SubnetworkID.Equal(subnetworkID) { continue } - - if qtype == dns.TypeA && node.Addr.IP.To4() == nil { - continue - } else if qtype == dns.TypeAAAA && node.Addr.IP.To4() != nil { + if qtype == dns.TypeA && !isIPv4(node.Addr) || + qtype == dns.TypeAAAA && isIPv4(node.Addr) { continue } - - if node.LastSuccess.IsZero() || - now.Sub(node.LastSuccess) > defaultStaleTimeout { + if !isGood(node) { continue } - addrs = append(addrs, node.Addr) i-- } @@ -192,9 +184,9 @@ func (m *Manager) GoodAddresses(qtype uint16, includeAllSubnetworks bool, subnet } // Attempt updates the last connection attempt for the specified ip address to now -func (m *Manager) Attempt(ip net.IP) { +func (m *Manager) Attempt(addr *appmessage.NetAddress) { m.mtx.Lock() - node, exists := m.nodes[ip.String()] + node, exists := m.nodes[addr.IP.String()+"_"+strconv.Itoa(int(addr.Port))] if exists { node.LastAttempt = time.Now() } @@ -202,10 +194,11 @@ func (m *Manager) Attempt(ip net.IP) { } // Good updates the last successful connection attempt for the specified ip address to now -func (m *Manager) Good(ip net.IP, subnetworkid *externalapi.DomainSubnetworkID) { +func (m *Manager) Good(addr *appmessage.NetAddress, userAgent *string, subnetworkid *externalapi.DomainSubnetworkID) { m.mtx.Lock() - node, exists := m.nodes[ip.String()] + node, exists := m.nodes[addr.IP.String()+"_"+strconv.Itoa(int(addr.Port))] if exists { + node.UserAgent = userAgent node.LastSuccess = time.Now() node.SubnetworkID = subnetworkid } @@ -233,37 +226,35 @@ out: } log.Infof("Address manager: saving peers") m.savePeers() - log.Infof("Address manager shoutdown") + log.Infof("Address manager shutdown") } func (m *Manager) prunePeers() { - var count int - now := time.Now() + var pruned, good, stale, bad, ipv4, ipv6 int m.mtx.Lock() - lastSeenAbovePruneExpire := func(node *Node) bool { - return now.Sub(node.LastSeen) > pruneExpireTimeout - } - hadAttemptsButNoSuccess := func(node *Node) bool { - return !node.LastAttempt.IsZero() && node.LastSuccess.IsZero() - } - hadSuccessButLongTimeAgo := func(node *Node) bool { - return !node.LastSuccess.IsZero() && now.Sub(node.LastSuccess) > pruneExpireTimeout - } - for k, node := range m.nodes { - if lastSeenAbovePruneExpire(node) || - hadAttemptsButNoSuccess(node) || - hadSuccessButLongTimeAgo(node) { - + if isExpired(node) { delete(m.nodes, k) - count++ + pruned++ + } else if isGood(node) { + good++ + if isIPv4(node.Addr) { + ipv4++ + } else { + ipv6++ + } + } else if isStale(node) { + stale++ + } else { + bad++ } } - l := len(m.nodes) + total := len(m.nodes) m.mtx.Unlock() - log.Infof("Pruned %d addresses: %d remaining", count, l) + log.Infof("Pruned %d addresses. %d left.", pruned, total) + log.Infof("Known nodes: Good:%d [4:%d, 6:%d] Stale:%d Bad:%d", good, ipv4, ipv6, stale, bad) } func (m *Manager) deserializePeers() error { @@ -320,3 +311,25 @@ func (m *Manager) savePeers() { return } } + +func isGood(node *Node) bool { + return !isNonDefaultPort(node.Addr) && time.Since(node.LastSuccess) < defaultStaleGoodTimeout +} + +func isStale(node *Node) bool { + return !node.LastSuccess.IsZero() && time.Since(node.LastAttempt) > defaultStaleGoodTimeout || + time.Since(node.LastAttempt) > defaultStaleBadTimeout +} + +func isExpired(node *Node) bool { + return time.Since(node.LastSeen) > pruneExpireTimeout && + time.Since(node.LastSuccess) > pruneExpireTimeout +} + +func isNonDefaultPort(addr *appmessage.NetAddress) bool { + return addr.Port != uint16(peersDefaultPort) +} + +func isIPv4(addr *appmessage.NetAddress) bool { + return addr.IP.To4() != nil +} diff --git a/netadapter/dnsseed_net_adapter.go b/netadapter/dnsseed_net_adapter.go new file mode 100644 index 0000000..b6f95b0 --- /dev/null +++ b/netadapter/dnsseed_net_adapter.go @@ -0,0 +1,237 @@ +package netadapter + +import ( + "fmt" + "github.com/spectre-project/dnsseeder/version" + "sync" + + "github.com/spectre-project/spectred/app/protocol/common" + "github.com/spectre-project/spectred/util/mstime" + + "github.com/spectre-project/spectred/infrastructure/network/netadapter/id" + + "github.com/spectre-project/spectred/app/appmessage" + "github.com/spectre-project/spectred/infrastructure/network/netadapter/router" + + "github.com/spectre-project/spectred/infrastructure/config" + "github.com/spectre-project/spectred/infrastructure/network/netadapter" + + "github.com/pkg/errors" +) + +// DnsseedNetAdapter allows tests and other tools to use a simple network adapter without implementing +// all the required supporting structures. +type DnsseedNetAdapter struct { + cfg *config.Config + lock sync.Mutex + netAdapter *netadapter.NetAdapter + routesChan <-chan *Routes +} + +// NewDnsseedNetAdapter creates a new instance of a DnsseedNetAdapter +func NewDnsseedNetAdapter(cfg *config.Config) (*DnsseedNetAdapter, error) { + netAdapter, err := netadapter.NewNetAdapter(cfg) + if err != nil { + return nil, errors.Wrap(err, "Error starting netAdapter") + } + + routerInitializer, routesChan := generateRouteInitializer() + + netAdapter.SetP2PRouterInitializer(routerInitializer) + netAdapter.SetRPCRouterInitializer(func(_ *router.Router, _ *netadapter.NetConnection) { + }) + + err = netAdapter.Start() + if err != nil { + return nil, errors.Wrap(err, "Error starting netAdapter") + } + + return &DnsseedNetAdapter{ + cfg: cfg, + lock: sync.Mutex{}, + netAdapter: netAdapter, + routesChan: routesChan, + }, nil +} + +// Connect opens a connection to the given address, handles handshake, and returns the routes for this connection +// To simplify usage the return type contains only two routes: +// OutgoingRoute - for all outgoing messages +// IncomingRoute - for all incoming messages (excluding handshake messages) +func (mna *DnsseedNetAdapter) Connect(address string) (*Routes, *appmessage.MsgVersion, error) { + mna.lock.Lock() + defer mna.lock.Unlock() + + err := mna.netAdapter.P2PConnect(address) + if err != nil { + return nil, nil, err + } + + routes := <-mna.routesChan + msgVersion, err := mna.handleHandshake(routes, mna.netAdapter.ID()) + if err != nil { + return nil, nil, errors.Wrap(err, "Error in handshake") + } + + spawn("netAdapterMock-handlePingPong", func() { + err := mna.handlePingPong(routes) + if err != nil { + panic(errors.Wrap(err, "Error from ping-pong")) + } + }) + + return routes, msgVersion, nil +} + +// handlePingPong makes sure that we are not disconnected due to not responding to pings. +// However, it only responds to pings, not sending its own, to conform to the minimal-ness +// of DnsseedNetAdapter +func (*DnsseedNetAdapter) handlePingPong(routes *Routes) error { + for { + message, err := routes.pingRoute.Dequeue() + if err != nil { + if errors.Is(err, router.ErrRouteClosed) { + return nil + } + return err + } + + pingMessage := message.(*appmessage.MsgPing) + + err = routes.OutgoingRoute.Enqueue(&appmessage.MsgPong{Nonce: pingMessage.Nonce}) + if err != nil { + return err + } + } +} + +func (mna *DnsseedNetAdapter) handleHandshake(routes *Routes, ourID *id.ID) (*appmessage.MsgVersion, error) { + msg, err := routes.handshakeRoute.DequeueWithTimeout(common.DefaultTimeout) + if err != nil { + return nil, err + } + msgVersion, ok := msg.(*appmessage.MsgVersion) + if !ok { + return nil, errors.Errorf("expected first message to be of type %s, but got %s", appmessage.CmdVersion, msg.Command()) + } + err = routes.OutgoingRoute.Enqueue(&appmessage.MsgVersion{ + ProtocolVersion: msgVersion.ProtocolVersion, + Network: mna.cfg.ActiveNetParams.Name, + Services: msgVersion.Services, + Timestamp: mstime.Now(), + Address: nil, + ID: ourID, + UserAgent: fmt.Sprintf("/spectre-dnsseeder:%s/", version.Version()), + DisableRelayTx: true, + SubnetworkID: nil, + }) + if err != nil { + return msgVersion, err + } + + msg, err = routes.handshakeRoute.DequeueWithTimeout(common.DefaultTimeout) + if err != nil { + return msgVersion, err + } + _, ok = msg.(*appmessage.MsgVerAck) + if !ok { + return msgVersion, errors.Errorf("expected second message to be of type %s, but got %s", appmessage.CmdVerAck, msg.Command()) + } + err = routes.OutgoingRoute.Enqueue(&appmessage.MsgVerAck{}) + if err != nil { + return msgVersion, err + } + + msg, err = routes.addressesRoute.DequeueWithTimeout(common.DefaultTimeout) + if err != nil { + return msgVersion, err + } + _, ok = msg.(*appmessage.MsgRequestAddresses) + if !ok { + return msgVersion, errors.Errorf("expected third message to be of type %s, but got %s", appmessage.CmdRequestAddresses, msg.Command()) + } + err = routes.OutgoingRoute.Enqueue(&appmessage.MsgAddresses{ + AddressList: []*appmessage.NetAddress{}, + }) + if err != nil { + return msgVersion, err + } + + err = routes.OutgoingRoute.Enqueue(&appmessage.MsgRequestAddresses{ + IncludeAllSubnetworks: true, + SubnetworkID: nil, + }) + if err != nil { + return msgVersion, err + } + msg, err = routes.addressesRoute.DequeueWithTimeout(common.DefaultTimeout) + if err != nil { + return msgVersion, err + } + _, ok = msg.(*appmessage.MsgAddresses) + if !ok { + return msgVersion, errors.Errorf("expected fourth message to be of type %s, but got %s", appmessage.CmdAddresses, msg.Command()) + } + + return msgVersion, nil +} + +func generateRouteInitializer() (netadapter.RouterInitializer, <-chan *Routes) { + cmdsWithBuiltInRoutes := []appmessage.MessageCommand{ + appmessage.CmdVersion, + appmessage.CmdVerAck, + appmessage.CmdRequestAddresses, + appmessage.CmdAddresses, + appmessage.CmdPing} + + everythingElse := make([]appmessage.MessageCommand, 0, len(appmessage.ProtocolMessageCommandToString)-len(cmdsWithBuiltInRoutes)) +outerLoop: + for command := range appmessage.ProtocolMessageCommandToString { + for _, cmdWithBuiltInRoute := range cmdsWithBuiltInRoutes { + if command == cmdWithBuiltInRoute { + continue outerLoop + } + } + + everythingElse = append(everythingElse, command) + } + + routesChan := make(chan *Routes) + + routeInitializer := func(router *router.Router, netConnection *netadapter.NetConnection) { + handshakeRoute, err := router.AddIncomingRoute("handshake", []appmessage.MessageCommand{appmessage.CmdVersion, appmessage.CmdVerAck}) + if err != nil { + panic(errors.Wrap(err, "error registering handshake route")) + } + addressesRoute, err := router.AddIncomingRoute("addresses", []appmessage.MessageCommand{appmessage.CmdRequestAddresses, appmessage.CmdAddresses}) + if err != nil { + panic(errors.Wrap(err, "error registering addresses route")) + } + pingRoute, err := router.AddIncomingRoute("ping", []appmessage.MessageCommand{appmessage.CmdPing}) + if err != nil { + panic(errors.Wrap(err, "error registering ping route")) + } + everythingElseRoute, err := router.AddIncomingRoute("everything else", everythingElse) + if err != nil { + panic(errors.Wrap(err, "error registering everythingElseRoute")) + } + + err = router.OutgoingRoute().Enqueue(appmessage.NewMsgReady()) + if err != nil { + panic(errors.Wrap(err, "error sending ready message")) + } + + spawn("netAdapterMock-routeInitializer-sendRoutesToChan", func() { + routesChan <- &Routes{ + netConnection: netConnection, + OutgoingRoute: router.OutgoingRoute(), + IncomingRoute: everythingElseRoute, + handshakeRoute: handshakeRoute, + addressesRoute: addressesRoute, + pingRoute: pingRoute, + } + }) + } + + return routeInitializer, routesChan +} diff --git a/netadapter/log.go b/netadapter/log.go new file mode 100644 index 0000000..aaf079c --- /dev/null +++ b/netadapter/log.go @@ -0,0 +1,9 @@ +package netadapter + +import ( + "github.com/spectre-project/spectred/infrastructure/logger" + "github.com/spectre-project/spectred/util/panics" +) + +var log = logger.RegisterSubSystem("NTAR") +var spawn = panics.GoroutineWrapperFunc(log) diff --git a/netadapter/routes.go b/netadapter/routes.go new file mode 100644 index 0000000..262ebff --- /dev/null +++ b/netadapter/routes.go @@ -0,0 +1,69 @@ +package netadapter + +import ( + "time" + + "github.com/spectre-project/spectred/infrastructure/network/netadapter" + + "github.com/pkg/errors" + + "github.com/spectre-project/spectred/app/appmessage" + "github.com/spectre-project/spectred/infrastructure/network/netadapter/router" +) + +// Routes holds the incoming and outgoing routes of a connection created by MinimalNetAdapter +type Routes struct { + netConnection *netadapter.NetConnection + IncomingRoute, OutgoingRoute *router.Route + handshakeRoute *router.Route + addressesRoute *router.Route + pingRoute *router.Route +} + +// WaitForMessageOfType waits for a message of requested type up to `timeout`, skipping all messages of any other type +// received while waiting +func (r *Routes) WaitForMessageOfType(command appmessage.MessageCommand, timeout time.Duration) (appmessage.Message, error) { + timeoutTime := time.Now().Add(timeout) + for { + route := r.chooseRouteForCommand(command) + message, err := route.DequeueWithTimeout(timeoutTime.Sub(time.Now())) + if err != nil { + return nil, errors.Wrapf(err, "error waiting for message of type %s", command) + } + if message.Command() == command { + return message, nil + } + } +} + +func (r *Routes) chooseRouteForCommand(command appmessage.MessageCommand) *router.Route { + switch command { + case appmessage.CmdVersion, appmessage.CmdVerAck: + return r.handshakeRoute + case appmessage.CmdRequestAddresses, appmessage.CmdAddresses: + return r.addressesRoute + case appmessage.CmdPing: + return r.pingRoute + default: + return r.IncomingRoute + } +} + +// WaitForDisconnect waits for a disconnect up to `timeout`, skipping all messages received while waiting +func (r *Routes) WaitForDisconnect(timeout time.Duration) error { + timeoutTime := time.Now().Add(timeout) + for { + _, err := r.IncomingRoute.DequeueWithTimeout(timeoutTime.Sub(time.Now())) + if errors.Is(err, router.ErrRouteClosed) { + return nil + } + if err != nil { + return errors.Wrap(err, "error waiting for disconnect") + } + } +} + +// Disconnect closes the connection behind the routes, thus closing all routes +func (r *Routes) Disconnect() { + r.netConnection.Disconnect() +} diff --git a/version/version.go b/version/version.go index 60c5912..34e14d6 100644 --- a/version/version.go +++ b/version/version.go @@ -11,7 +11,7 @@ const validCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs const ( appMajor uint = 0 appMinor uint = 3 - appPatch uint = 14 + appPatch uint = 15 ) // appBuild is defined as a variable so it can be overridden during the build