Skip to content

Commit

Permalink
Support dynamic port peerlist gossip (#2603)
Browse files Browse the repository at this point in the history
  • Loading branch information
StephenButtolph committed Jan 13, 2024
1 parent 62fe36d commit a6ee978
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 213 deletions.
21 changes: 11 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,17 @@ jobs:
- name: Build AvalancheGo Binary
shell: bash
run: ./scripts/build.sh
- name: Run e2e tests
shell: bash
run: ./scripts/tests.upgrade.sh
- name: Upload tmpnet network dir
uses: actions/upload-artifact@v3
if: always()
with:
name: upgrade-tmpnet-data
path: ${{ env.tmpnet_data_path }}
if-no-files-found: error
# TODO: re-activate this test after there is a compatible tag to use
# - name: Run e2e tests
# shell: bash
# run: ./scripts/tests.upgrade.sh
# - name: Upload tmpnet network dir
# uses: actions/upload-artifact@v3
# if: always()
# with:
# name: upgrade-tmpnet-data
# path: ${{ env.tmpnet_data_path }}
# if-no-files-found: error
Lint:
runs-on: ubuntu-latest
steps:
Expand Down
101 changes: 2 additions & 99 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,20 @@ import (

"golang.org/x/sync/errgroup"

"github.com/ava-labs/avalanchego/nat"
"github.com/ava-labs/avalanchego/node"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/ips"
"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/avalanchego/utils/perms"
"github.com/ava-labs/avalanchego/utils/ulimit"
)

const (
Header = ` _____ .__ .__
const Header = ` _____ .__ .__
/ _ \___ _______ | | _____ ____ ____ | |__ ____ ,_ o
/ /_\ \ \/ /\__ \ | | \__ \ / \_/ ___\| | \_/ __ \ / //\,
/ | \ / / __ \| |__/ __ \| | \ \___| Y \ ___/ \>> |
\____|__ /\_/ (____ /____(____ /___| /\___ >___| /\___ > \\
\/ \/ \/ \/ \/ \/ \/`
)

var (
stakingPortName = fmt.Sprintf("%s-staking", constants.AppName)
httpPortName = fmt.Sprintf("%s-http", constants.AppName)

_ App = (*app)(nil)
)
var _ App = (*app)(nil)

type App interface {
// Start kicks off the application and returns immediately.
Expand Down Expand Up @@ -88,7 +78,6 @@ func New(config node.Config) (App, error) {
}

return &app{
config: config,
node: n,
log: log,
logFactory: logFactory,
Expand Down Expand Up @@ -133,7 +122,6 @@ func Run(app App) int {

// app is a wrapper around a node that runs in this process
type app struct {
config node.Config
node *node.Node
log logging.Logger
logFactory logging.Factory
Expand All @@ -144,88 +132,6 @@ type app struct {
// Does not block until the node is done. Errors returned from this method
// are not logged.
func (a *app) Start() error {
// Track if sybil control is enforced
if !a.config.SybilProtectionEnabled {
a.log.Warn("sybil control is not enforced")
}

// TODO move this to config
// SupportsNAT() for NoRouter is false.
// Which means we tried to perform a NAT activity but we were not successful.
if a.config.AttemptedNATTraversal && !a.config.Nat.SupportsNAT() {
a.log.Warn("UPnP and NAT-PMP router attach failed, " +
"you may not be listening publicly. " +
"Please confirm the settings in your router")
}

if ip := a.config.IPPort.IPPort().IP; ip.IsLoopback() || ip.IsPrivate() {
a.log.Warn("P2P IP is private, you will not be publicly discoverable",
zap.Stringer("ip", ip),
)
}

// An empty host is treated as a wildcard to match all addresses, so it is
// considered public.
hostIsPublic := a.config.HTTPHost == ""
if !hostIsPublic {
ip, err := ips.Lookup(a.config.HTTPHost)
if err != nil {
a.log.Fatal("failed to lookup HTTP host",
zap.String("host", a.config.HTTPHost),
zap.Error(err),
)
a.logFactory.Close()
return err
}
hostIsPublic = !ip.IsLoopback() && !ip.IsPrivate()

a.log.Debug("finished HTTP host lookup",
zap.String("host", a.config.HTTPHost),
zap.Stringer("ip", ip),
zap.Bool("isPublic", hostIsPublic),
)
}

mapper := nat.NewPortMapper(a.log, a.config.Nat)

// Open staking port we want for NAT traversal to have the external port
// (config.IP.Port) to connect to our internal listening port
// (config.InternalStakingPort) which should be the same in most cases.
if port := a.config.IPPort.IPPort().Port; port != 0 {
mapper.Map(
port,
port,
stakingPortName,
a.config.IPPort,
a.config.IPResolutionFreq,
)
}

// Don't open the HTTP port if the HTTP server is private
if hostIsPublic {
a.log.Warn("HTTP server is binding to a potentially public host. "+
"You may be vulnerable to a DoS attack if your HTTP port is publicly accessible",
zap.String("host", a.config.HTTPHost),
)

// For NAT traversal we want to route from the external port
// (config.ExternalHTTPPort) to our internal port (config.HTTPPort).
if a.config.HTTPPort != 0 {
mapper.Map(
a.config.HTTPPort,
a.config.HTTPPort,
httpPortName,
nil,
a.config.IPResolutionFreq,
)
}
}

// Regularly update our public IP.
// Note that if the node config said to not dynamically resolve and
// update our public IP, [p.config.IPUdater] is a no-op implementation.
go a.config.IPUpdater.Dispatch(a.log)

// [p.ExitCode] will block until [p.exitWG.Done] is called
a.exitWG.Add(1)
go func() {
Expand All @@ -238,9 +144,6 @@ func (a *app) Start() error {
a.exitWG.Done()
}()
defer func() {
mapper.UnmapAllPorts()
a.config.IPUpdater.Stop()

// If [p.node.Dispatch()] panics, then we should log the panic and
// then re-raise the panic. This is why the above defer is broken
// into two parts.
Expand Down
68 changes: 9 additions & 59 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
package config

import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/fs"
"math"
"net"
"os"
"path/filepath"
"strings"
Expand All @@ -25,7 +23,6 @@ import (
"github.com/ava-labs/avalanchego/genesis"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/ipcs"
"github.com/ava-labs/avalanchego/nat"
"github.com/ava-labs/avalanchego/network"
"github.com/ava-labs/avalanchego/network/dialer"
"github.com/ava-labs/avalanchego/network/throttling"
Expand All @@ -40,7 +37,6 @@ import (
"github.com/ava-labs/avalanchego/utils/compression"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/crypto/bls"
"github.com/ava-labs/avalanchego/utils/dynamicip"
"github.com/ava-labs/avalanchego/utils/ips"
"github.com/ava-labs/avalanchego/utils/logging"
"github.com/ava-labs/avalanchego/utils/password"
Expand All @@ -58,7 +54,6 @@ const (
chainConfigFileName = "config"
chainUpgradeFileName = "upgrade"
subnetConfigFileExt = ".json"
ipResolutionTimeout = 30 * time.Second

ipcDeprecationMsg = "IPC API is deprecated"
keystoreDeprecationMsg = "keystore API is deprecated"
Expand Down Expand Up @@ -617,64 +612,19 @@ func getBootstrapConfig(v *viper.Viper, networkID uint32) (node.BootstrapConfig,
}

func getIPConfig(v *viper.Viper) (node.IPConfig, error) {
ipResolutionService := v.GetString(PublicIPResolutionServiceKey)
ipResolutionFreq := v.GetDuration(PublicIPResolutionFreqKey)
if ipResolutionFreq <= 0 {
return node.IPConfig{}, fmt.Errorf("%q must be > 0", PublicIPResolutionFreqKey)
}

stakingPort := uint16(v.GetUint(StakingPortKey))
publicIP := v.GetString(PublicIPKey)
if publicIP != "" && ipResolutionService != "" {
return node.IPConfig{}, fmt.Errorf("only one of --%s and --%s can be given", PublicIPKey, PublicIPResolutionServiceKey)
}

// Define default configuration
ipConfig := node.IPConfig{
IPUpdater: dynamicip.NewNoUpdater(),
IPResolutionFreq: ipResolutionFreq,
Nat: nat.NewNoRouter(),
ListenHost: v.GetString(StakingHostKey),
}

if publicIP != "" {
// User specified a specific public IP to use.
ip := net.ParseIP(publicIP)
if ip == nil {
return node.IPConfig{}, fmt.Errorf("invalid IP Address %s", publicIP)
}
ipConfig.IPPort = ips.NewDynamicIPPort(ip, stakingPort)
return ipConfig, nil
PublicIP: v.GetString(PublicIPKey),
PublicIPResolutionService: v.GetString(PublicIPResolutionServiceKey),
PublicIPResolutionFreq: v.GetDuration(PublicIPResolutionFreqKey),
ListenHost: v.GetString(StakingHostKey),
ListenPort: uint16(v.GetUint(StakingPortKey)),
}
if ipResolutionService != "" {
// User specified to use dynamic IP resolution.
resolver, err := dynamicip.NewResolver(ipResolutionService)
if err != nil {
return node.IPConfig{}, fmt.Errorf("couldn't create IP resolver: %w", err)
}

// Use that to resolve our public IP.
ctx, cancel := context.WithTimeout(context.Background(), ipResolutionTimeout)
defer cancel()
ip, err := resolver.Resolve(ctx)
if err != nil {
return node.IPConfig{}, fmt.Errorf("couldn't resolve public IP: %w", err)
}
ipConfig.IPPort = ips.NewDynamicIPPort(ip, stakingPort)
ipConfig.IPUpdater = dynamicip.NewUpdater(ipConfig.IPPort, resolver, ipResolutionFreq)
return ipConfig, nil
if ipConfig.PublicIPResolutionFreq <= 0 {
return node.IPConfig{}, fmt.Errorf("%q must be > 0", PublicIPResolutionFreqKey)
}

// User didn't specify a public IP to use, and they didn't specify a public IP resolution
// service to use. Try to resolve public IP with NAT traversal.
nat := nat.GetRouter()
ip, err := nat.ExternalIP()
if err != nil {
return node.IPConfig{}, fmt.Errorf("public IP / IP resolution service not given and failed to resolve IP with NAT: %w", err)
if ipConfig.PublicIP != "" && ipConfig.PublicIPResolutionService != "" {
return node.IPConfig{}, fmt.Errorf("only one of --%s and --%s can be given", PublicIPKey, PublicIPResolutionServiceKey)
}
ipConfig.IPPort = ips.NewDynamicIPPort(ip, stakingPort)
ipConfig.Nat = nat
ipConfig.AttemptedNATTraversal = true
return ipConfig, nil
}

Expand Down
5 changes: 3 additions & 2 deletions config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/ava-labs/avalanchego/trace"
"github.com/ava-labs/avalanchego/utils/compression"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/utils/dynamicip"
"github.com/ava-labs/avalanchego/utils/ulimit"
"github.com/ava-labs/avalanchego/utils/units"
)
Expand Down Expand Up @@ -133,9 +134,9 @@ func addNodeFlags(fs *pflag.FlagSet) {
fs.Duration(NetworkPeerListGossipFreqKey, constants.DefaultNetworkPeerListGossipFreq, "Frequency to gossip peers to other nodes")

// Public IP Resolution
fs.String(PublicIPKey, "", "Public IP of this node for P2P communication. If empty, try to discover with NAT")
fs.String(PublicIPKey, "", "Public IP of this node for P2P communication")
fs.Duration(PublicIPResolutionFreqKey, 5*time.Minute, "Frequency at which this node resolves/updates its public IP and renew NAT mappings, if applicable")
fs.String(PublicIPResolutionServiceKey, "", fmt.Sprintf("Only acceptable values are 'ifconfigco', 'opendns' or 'ifconfigme'. When provided, the node will use that service to periodically resolve/update its public IP. Ignored if %s is set", PublicIPKey))
fs.String(PublicIPResolutionServiceKey, "", fmt.Sprintf("Only acceptable values are %q, %q or %q. When provided, the node will use that service to periodically resolve/update its public IP", dynamicip.OpenDNSName, dynamicip.IFConfigCoName, dynamicip.IFConfigMeName))

// Inbound Connection Throttling
fs.Duration(NetworkInboundConnUpgradeThrottlerCooldownKey, constants.DefaultInboundConnUpgradeThrottlerCooldown, "Upgrade an inbound connection from a given IP at most once per this duration. If 0, don't rate-limit inbound connection upgrades")
Expand Down
4 changes: 2 additions & 2 deletions nat/nat.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ type Mapper struct {
}

// NewPortMapper returns an initialized mapper
func NewPortMapper(log logging.Logger, r Router) Mapper {
return Mapper{
func NewPortMapper(log logging.Logger, r Router) *Mapper {
return &Mapper{
log: log,
r: r,
closer: make(chan struct{}),
Expand Down
10 changes: 5 additions & 5 deletions network/dialer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (d *testDialer) NewListener() (ips.DynamicIPPort, *testListener) {
// Uses a private IP to easily enable testing AllowPrivateIPs
ip := ips.NewDynamicIPPort(
net.IPv4(10, 0, 0, 0),
uint16(len(d.listeners)),
uint16(len(d.listeners)+1),
)
staticIP := ip.IPPort()
listener := newTestListener(staticIP)
Expand All @@ -55,22 +55,22 @@ func (d *testDialer) Dial(ctx context.Context, ip ips.IPPort) (net.Conn, error)
Conn: serverConn,
localAddr: &net.TCPAddr{
IP: net.IPv6loopback,
Port: 0,
Port: 1,
},
remoteAddr: &net.TCPAddr{
IP: net.IPv6loopback,
Port: 1,
Port: 2,
},
}
client := &testConn{
Conn: clientConn,
localAddr: &net.TCPAddr{
IP: net.IPv6loopback,
Port: 2,
Port: 3,
},
remoteAddr: &net.TCPAddr{
IP: net.IPv6loopback,
Port: 3,
Port: 4,
},
}
select {
Expand Down
Loading

0 comments on commit a6ee978

Please sign in to comment.