diff --git a/rocketpool-cli/service/config/settings-native-smartnode.go b/rocketpool-cli/service/config/settings-native-smartnode.go index e2800f585..38cffb2af 100644 --- a/rocketpool-cli/service/config/settings-native-smartnode.go +++ b/rocketpool-cli/service/config/settings-native-smartnode.go @@ -23,7 +23,7 @@ func NewNativeSmartnodeConfigPage(home *settingsNativeHome) *NativeSmartnodeConf configPage.page = newPage( home.homePage, "settings-native-smartnode", - "Smartnode and TX Fees", + "Smart Node and TX Fees", "Select this to configure the settings for the Smart Node itself, including the defaults and limits on transaction fees.", configPage.layout.grid, ) @@ -43,7 +43,8 @@ func (configPage *NativeSmartnodeConfigPage) createContent() { layout.setupEscapeReturnHomeHandler(configPage.home.md, configPage.home.homePage) // Set up the form items - formItems := createParameterizedFormItems(masterConfig.Smartnode.GetParameters(), layout.descriptionBox) + params := append(masterConfig.Smartnode.GetParameters(), &masterConfig.EnableIPv6, &masterConfig.Alertmanager.ShowAlertsOnCLI) + formItems := createParameterizedFormItems(params, layout.descriptionBox) for _, formItem := range formItems { if formItem.parameter.ID == config.ProjectNameID { // Ignore the project name ID since it doesn't apply to native mode diff --git a/rocketpool-cli/service/config/settings-smartnode.go b/rocketpool-cli/service/config/settings-smartnode.go index 10472b053..1a011cb99 100644 --- a/rocketpool-cli/service/config/settings-smartnode.go +++ b/rocketpool-cli/service/config/settings-smartnode.go @@ -25,7 +25,7 @@ func NewSmartnodeConfigPage(home *settingsHome) *SmartnodeConfigPage { configPage.page = newPage( home.homePage, "settings-smartnode", - "Smartnode and TX Fees", + "Smart Node and TX Fees", "Select this to configure the settings for the Smart Node itself, including the defaults and limits on transaction fees.", configPage.layout.grid, ) @@ -68,7 +68,8 @@ func (configPage *SmartnodeConfigPage) createContent() { }) // Set up the form items - formItems := createParameterizedFormItems(masterConfig.Smartnode.GetParameters(), layout.descriptionBox) + params := append(masterConfig.Smartnode.GetParameters(), &masterConfig.EnableIPv6, &masterConfig.Alertmanager.ShowAlertsOnCLI) + formItems := createParameterizedFormItems(params, layout.descriptionBox) for _, formItem := range formItems { layout.form.AddFormItem(formItem.item) layout.parameters[formItem.item] = formItem diff --git a/rocketpool-cli/service/service.go b/rocketpool-cli/service/service.go index f316a017e..a3bab8ef6 100644 --- a/rocketpool-cli/service/service.go +++ b/rocketpool-cli/service/service.go @@ -328,6 +328,12 @@ func configureService(configPath string, isNative, yes bool, composeFiles []stri }) } + // Warn if IPv6 is enabled but no public IPv6 address is available + if md.Config.IsIPv6Enabled() && md.Config.GetExternalIpv6() == "" { + color.YellowPrintln("Warning: IPv6 is enabled but no public IPv6 address was detected. Your node will not be reachable by IPv6 peers.") + fmt.Println() + } + // Query for service start if this is old and there are containers to change if len(md.ContainersToRestart) > 0 { fmt.Println("The following containers must be restarted for the changes to take effect:") @@ -520,6 +526,12 @@ func startService(params startServiceParams) error { return fmt.Errorf("No configuration detected. Please run `rocketpool service config` to set up your Smart Node before running it.") } + // Warn if IPv6 is enabled but no public IPv6 address is available + if cfg.IsIPv6Enabled() && cfg.GetExternalIpv6() == "" { + color.YellowPrintln("Warning: IPv6 is enabled but no public IPv6 address was detected. Your node will not be reachable by IPv6 peers.") + fmt.Println() + } + // Check if this is a new install isUpdate, err := rp.IsFirstRun() if err != nil { diff --git a/shared/services/config/rocket-pool-config.go b/shared/services/config/rocket-pool-config.go index 08854ec00..5d63252d4 100644 --- a/shared/services/config/rocket-pool-config.go +++ b/shared/services/config/rocket-pool-config.go @@ -76,6 +76,9 @@ type RocketPoolConfig struct { ConsensusClient config.Parameter `yaml:"consensusClient,omitempty"` ExternalConsensusClient config.Parameter `yaml:"externalConsensusClient,omitempty"` + // IPv6 networking + EnableIPv6 config.Parameter `yaml:"enableIPv6,omitempty"` + // Metrics settings EnableMetrics config.Parameter `yaml:"enableMetrics,omitempty"` EnableODaoMetrics config.Parameter `yaml:"enableODaoMetrics,omitempty"` @@ -352,6 +355,17 @@ func NewRocketPoolConfig(rpDir string, isNativeMode bool) *RocketPoolConfig { }}, }, + EnableIPv6: config.Parameter{ + ID: "enableIPv6", + Name: "Enable IPv6", + Description: "Enables dual-stack (IPv4 + IPv6) networking for the Smart Node. When enabled, your Ethereum clients will listen on both IPv4 and IPv6 and can peer with IPv6 nodes in addition to IPv4. Enable this if your machine has only an IPv6 address, or if you want your node to participate in IPv6 peering.", + Type: config.ParameterType_Bool, + Default: map[config.Network]interface{}{config.Network_All: false}, + AffectsContainers: []config.ContainerID{config.ContainerID_Api, config.ContainerID_Node, config.ContainerID_Watchtower, config.ContainerID_Eth1, config.ContainerID_Eth2, config.ContainerID_Validator, config.ContainerID_Grafana, config.ContainerID_Prometheus, config.ContainerID_Alertmanager, config.ContainerID_Exporter, config.ContainerID_MevBoost, config.ContainerID_CommitBoost}, + CanBeBlank: false, + OverwriteOnUpgrade: false, + }, + EnableMetrics: config.Parameter{ ID: "enableMetrics", Name: "Enable Metrics", @@ -573,6 +587,7 @@ func (cfg *RocketPoolConfig) GetParameters() []*config.Parameter { &cfg.ConsensusClientMode, &cfg.ConsensusClient, &cfg.ExternalConsensusClient, + &cfg.EnableIPv6, &cfg.EnableMetrics, &cfg.EnableODaoMetrics, &cfg.EnableBitflyNodeMetrics, @@ -1327,6 +1342,12 @@ func (cfg *RocketPoolConfig) GetECAdditionalFlags() (string, error) { return "", fmt.Errorf("Unknown Execution Client %s", string(cfg.ExecutionClient.Value.(config.ExecutionClient))) } +// IsIPv6Enabled returns true if IPv6 support is enabled for the Docker network. +// Used by text/template to conditionally add enable_ipv6 to compose network definitions. +func (cfg *RocketPoolConfig) IsIPv6Enabled() bool { + return cfg.EnableIPv6.Value.(bool) +} + // Used by text/template to format eth1.yml func (cfg *RocketPoolConfig) GetExternalIp() string { // Get the external IP address @@ -1337,13 +1358,31 @@ func (cfg *RocketPoolConfig) GetExternalIp() string { return "" } - if ip.To4() == nil { - fmt.Println("Warning: external IP address is v6; if you're using Nimbus or Besu, it may have trouble finding peers:") + if ip.To4() == nil && !cfg.IsIPv6Enabled() { + fmt.Println("Warning: your external IP address is IPv6. If you haven't enabled IPv6 support in your configuration, your node may have trouble finding peers. Run 'rocketpool service config' and enable IPv6 under 'Smart Node and TX Fees'.") } return ip.String() } +// Used by text/template to format eth2.yml when IPv6 is enabled. +// Lodestar requires --enr.ip6 and Teku requires --p2p-advertised-ips to advertise IPv6 in their ENR. +// Returns the external IPv6 address, or empty string if unavailable. +func (cfg *RocketPoolConfig) GetExternalIpv6() string { + consensusConfig := externalip.ConsensusConfig{Timeout: 3 * time.Second} + ip6Consensus := externalip.DefaultConsensus(&consensusConfig, nil) + err := ip6Consensus.UseIPProtocol(6) + if err != nil { + return "" + } + + ip, err := ip6Consensus.ExternalIP() + if err != nil || ip.To4() != nil { + return "" + } + return ip.String() +} + // Gets the tag of the cc container // Used by text/template to format eth2.yml func (cfg *RocketPoolConfig) GetBeaconContainerTag() (string, error) { diff --git a/shared/services/config/teku-config.go b/shared/services/config/teku-config.go index e0e8af159..bd59dede0 100644 --- a/shared/services/config/teku-config.go +++ b/shared/services/config/teku-config.go @@ -7,9 +7,10 @@ import ( ) const ( - tekuTagTest string = "consensys/teku:26.4.0" - tekuTagProd string = "consensys/teku:26.4.0" - defaultTekuMaxPeers uint16 = 100 + tekuTagTest string = "consensys/teku:26.4.0" + tekuTagProd string = "consensys/teku:26.4.0" + defaultTekuMaxPeers uint16 = 100 + defaultTekuP2pIpv6Port uint16 = 9090 ) // Configuration for Teku @@ -37,6 +38,9 @@ type TekuConfig struct { // Custom command line flags for the BN AdditionalBnFlags config.Parameter `yaml:"additionalBnFlags,omitempty"` + // The IPv6 P2P port for Teku (Teku uses a separate port for IPv6) + P2pIpv6Port config.Parameter `yaml:"p2pIpv6Port,omitempty"` + // Custom command line flags for the VC AdditionalVcFlags config.Parameter `yaml:"additionalVcFlags,omitempty"` } @@ -121,6 +125,17 @@ func NewTekuConfig(cfg *RocketPoolConfig) *TekuConfig { OverwriteOnUpgrade: false, }, + P2pIpv6Port: config.Parameter{ + ID: "p2pIpv6Port", + Name: "P2P IPv6 Port", + Description: "The port Teku uses for P2P communication over IPv6. Teku requires a dedicated port for IPv6 (separate from the main P2P port). Only used when IPv6 support is enabled.", + Type: config.ParameterType_Uint16, + Default: map[config.Network]interface{}{config.Network_All: defaultTekuP2pIpv6Port}, + AffectsContainers: []config.ContainerID{config.ContainerID_Eth2}, + CanBeBlank: false, + OverwriteOnUpgrade: false, + }, + AdditionalVcFlags: config.Parameter{ ID: "additionalVcFlags", Name: "Additional Validator Client Flags", @@ -143,6 +158,7 @@ func (cfg *TekuConfig) GetParameters() []*config.Parameter { &cfg.UseSlashingProtection, &cfg.ContainerTag, &cfg.AdditionalBnFlags, + &cfg.P2pIpv6Port, &cfg.AdditionalVcFlags, } } diff --git a/shared/services/connectivity/check-port-connectivity.go b/shared/services/connectivity/check-port-connectivity.go index 9c8800d37..e5634d88c 100644 --- a/shared/services/connectivity/check-port-connectivity.go +++ b/shared/services/connectivity/check-port-connectivity.go @@ -30,9 +30,12 @@ var publicIPResolvers = []struct { addr string // host:port (IP literal) hostname string // special hostname that returns the caller's public IP }{ - {"208.67.222.222:53", "myip.opendns.com"}, // OpenDNS primary - {"208.67.220.220:53", "myip.opendns.com"}, // OpenDNS secondary - {"216.239.32.10:53", "o-o.myaddr.l.google.com"}, // Google ns1 + {"208.67.222.222:53", "myip.opendns.com"}, // OpenDNS primary (IPv4) + {"208.67.220.220:53", "myip.opendns.com"}, // OpenDNS secondary (IPv4) + {"216.239.32.10:53", "o-o.myaddr.l.google.com"}, // Google ns1 (IPv4) + {"[2620:119:35::35]:53", "myip.opendns.com"}, // OpenDNS primary (IPv6) + {"[2620:119:53::53]:53", "myip.opendns.com"}, // OpenDNS secondary (IPv6) + {"[2001:4860:4860::8888]:53", "o-o.myaddr.l.google.com"}, // Google ns1 (IPv6) } // Check port connectivity task diff --git a/shared/services/rocketpool/assets/install/scripts/start-bn.sh b/shared/services/rocketpool/assets/install/scripts/start-bn.sh index 9e23c3678..d58d3e5e9 100755 --- a/shared/services/rocketpool/assets/install/scripts/start-bn.sh +++ b/shared/services/rocketpool/assets/install/scripts/start-bn.sh @@ -99,6 +99,10 @@ if [ "$CC_CLIENT" = "lighthouse" ]; then CMD="$CMD --monitoring-endpoint $BITFLY_NODE_METRICS_ENDPOINT?apikey=$BITFLY_NODE_METRICS_SECRET&machine=$BITFLY_NODE_METRICS_MACHINE_NAME" fi + if [ "$ENABLE_IPV6" = "true" ]; then + CMD="$CMD --listen-address 0.0.0.0 --listen-address :: --port6 $BN_P2P_PORT --enr-udp6-port $BN_P2P_PORT --quic-port6 ${BN_P2P_QUIC_PORT:-8001}" + fi + exec ${CMD} fi @@ -142,7 +146,14 @@ if [ "$CC_CLIENT" = "lodestar" ]; then else CMD="$CMD --enr.ip $EXTERNAL_IP --nat" fi - fi + fi + + if [ "$ENABLE_IPV6" = "true" ]; then + CMD="$CMD --listenAddress 0.0.0.0 --listenAddress6 :: --port6 $BN_P2P_PORT --quicPort6 ${BN_P2P_QUIC_PORT:-8001}" + if [ ! -z "$EXTERNAL_IP6" ]; then + CMD="$CMD --enr.ip6 $EXTERNAL_IP6" + fi + fi if [ ! -z "$CHECKPOINT_SYNC_URL" ]; then CMD="$CMD --checkpointSyncUrl $CHECKPOINT_SYNC_URL" @@ -313,6 +324,17 @@ if [ "$CC_CLIENT" = "teku" ]; then CMD="$CMD --metrics-publish-endpoint=$BITFLY_NODE_METRICS_ENDPOINT?apikey=$BITFLY_NODE_METRICS_SECRET&machine=$BITFLY_NODE_METRICS_MACHINE_NAME" fi + if [ "$ENABLE_IPV6" = "true" ]; then + CMD="$CMD --p2p-interface=0.0.0.0,:: --p2p-port-ipv6=$BN_IPV6_P2P_PORT" + if [ ! -z "$EXTERNAL_IP6" ]; then + if [ ! -z "$EXTERNAL_IP" ] && ! expr "$EXTERNAL_IP" : '.*:' >/dev/null; then + CMD="$CMD --p2p-advertised-ips $EXTERNAL_IP,$EXTERNAL_IP6 --p2p-advertised-port-ipv6=$BN_IPV6_P2P_PORT" + else + CMD="$CMD --p2p-advertised-ips $EXTERNAL_IP6 --p2p-advertised-port-ipv6=$BN_IPV6_P2P_PORT" + fi + fi + fi + if [ "$TEKU_JVM_HEAP_SIZE" -gt "0" ]; then CMD="env JAVA_OPTS=\"-Xmx${TEKU_JVM_HEAP_SIZE}m\" $CMD" fi diff --git a/shared/services/rocketpool/assets/install/templates/alertmanager.tmpl b/shared/services/rocketpool/assets/install/templates/alertmanager.tmpl index 9f4544124..b82b55e54 100644 --- a/shared/services/rocketpool/assets/install/templates/alertmanager.tmpl +++ b/shared/services/rocketpool/assets/install/templates/alertmanager.tmpl @@ -21,5 +21,6 @@ services: - net networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} volumes: alertmanager-data: diff --git a/shared/services/rocketpool/assets/install/templates/commit-boost.tmpl b/shared/services/rocketpool/assets/install/templates/commit-boost.tmpl index 1e0393420..cf9380b39 100644 --- a/shared/services/rocketpool/assets/install/templates/commit-boost.tmpl +++ b/shared/services/rocketpool/assets/install/templates/commit-boost.tmpl @@ -29,3 +29,4 @@ services: - no-new-privileges networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} diff --git a/shared/services/rocketpool/assets/install/templates/eth1.tmpl b/shared/services/rocketpool/assets/install/templates/eth1.tmpl index 028960a92..da2e7178d 100644 --- a/shared/services/rocketpool/assets/install/templates/eth1.tmpl +++ b/shared/services/rocketpool/assets/install/templates/eth1.tmpl @@ -66,5 +66,6 @@ services: - no-new-privileges networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} volumes: eth1clientdata: diff --git a/shared/services/rocketpool/assets/install/templates/eth2.tmpl b/shared/services/rocketpool/assets/install/templates/eth2.tmpl index edfda8b14..7d75cba86 100644 --- a/shared/services/rocketpool/assets/install/templates/eth2.tmpl +++ b/shared/services/rocketpool/assets/install/templates/eth2.tmpl @@ -29,15 +29,32 @@ services: stop_grace_period: 3m {{- $p2p := (or .ConsensusCommon.P2pPort.String "9001")}} ports: + {{- if .IsIPv6Enabled}} + - "[::]:{{$p2p}}:{{$p2p}}/udp" + - "[::]:{{$p2p}}:{{$p2p}}/tcp" + {{- else}} - "{{$p2p}}:{{$p2p}}/udp" - "{{$p2p}}:{{$p2p}}/tcp" + {{- end}} {{- if eq .ConsensusClient.String "lighthouse"}} + {{- if .IsIPv6Enabled}} + - "[::]:{{.Lighthouse.P2pQuicPort}}:{{.Lighthouse.P2pQuicPort}}/udp" + {{- else}} - "{{.Lighthouse.P2pQuicPort}}:{{.Lighthouse.P2pQuicPort}}/udp" + {{- end}} {{- else if eq .ConsensusClient.String "prysm"}} - "{{.Prysm.P2pQuicPort}}:{{.Prysm.P2pQuicPort}}/udp" {{- else if eq .ConsensusClient.String "lodestar"}} + {{- if .IsIPv6Enabled}} + - "[::]:{{.Lodestar.P2pQuicPort}}:{{.Lodestar.P2pQuicPort}}/udp" + {{- else}} - "{{.Lodestar.P2pQuicPort}}:{{.Lodestar.P2pQuicPort}}/udp" {{- end}} + {{- end}} + {{- if and (eq .ConsensusClient.String "teku") .IsIPv6Enabled}} + - "{{.Teku.P2pIpv6Port}}:{{.Teku.P2pIpv6Port}}/udp" + - "{{.Teku.P2pIpv6Port}}:{{.Teku.P2pIpv6Port}}/tcp" + {{- end}} {{- range $entry := .GetBnOpenPorts}} - "{{$entry}}" {{- end}} @@ -66,6 +83,10 @@ services: - ENABLE_METRICS={{.EnableMetrics}} - BN_METRICS_PORT={{.BnMetricsPort}} - EXTERNAL_IP={{.GetExternalIp}} + {{- if .IsIPv6Enabled}} + - EXTERNAL_IP6={{.GetExternalIpv6}} + {{- end}} + - ENABLE_IPV6={{.IsIPv6Enabled}} - CHECKPOINT_SYNC_URL={{.ConsensusCommon.CheckpointSyncProvider}} - DOPPELGANGER_DETECTION={{.IsDoppelgangerEnabled}} - BN_ADDITIONAL_FLAGS={{.GetBNAdditionalFlags}} @@ -82,6 +103,7 @@ services: {{- if eq .ConsensusClient.String "teku"}} - TEKU_JVM_HEAP_SIZE={{.Teku.JvmHeapSize}} - TEKU_ARCHIVE_MODE={{.Teku.ArchiveMode}} + - BN_IPV6_P2P_PORT={{.Teku.P2pIpv6Port}} {{- else if eq .ConsensusClient.String "prysm"}} - BN_RPC_PORT={{.Prysm.RpcPort}} - BN_P2P_QUIC_PORT={{.Prysm.P2pQuicPort}} @@ -109,5 +131,6 @@ services: {{- end}} networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} volumes: eth2clientdata: diff --git a/shared/services/rocketpool/assets/install/templates/exporter.tmpl b/shared/services/rocketpool/assets/install/templates/exporter.tmpl index f254922f9..75df4c8d4 100644 --- a/shared/services/rocketpool/assets/install/templates/exporter.tmpl +++ b/shared/services/rocketpool/assets/install/templates/exporter.tmpl @@ -34,3 +34,4 @@ services: network_mode: host networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} diff --git a/shared/services/rocketpool/assets/install/templates/grafana.tmpl b/shared/services/rocketpool/assets/install/templates/grafana.tmpl index af983f657..de0467416 100644 --- a/shared/services/rocketpool/assets/install/templates/grafana.tmpl +++ b/shared/services/rocketpool/assets/install/templates/grafana.tmpl @@ -23,5 +23,6 @@ services: - net networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} volumes: grafana-storage: diff --git a/shared/services/rocketpool/assets/install/templates/mev-boost.tmpl b/shared/services/rocketpool/assets/install/templates/mev-boost.tmpl index 5e8345be8..ddacb307e 100644 --- a/shared/services/rocketpool/assets/install/templates/mev-boost.tmpl +++ b/shared/services/rocketpool/assets/install/templates/mev-boost.tmpl @@ -30,3 +30,4 @@ services: - no-new-privileges networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} diff --git a/shared/services/rocketpool/assets/install/templates/node.tmpl b/shared/services/rocketpool/assets/install/templates/node.tmpl index 6d6fb5992..482021489 100644 --- a/shared/services/rocketpool/assets/install/templates/node.tmpl +++ b/shared/services/rocketpool/assets/install/templates/node.tmpl @@ -27,3 +27,4 @@ services: - no-new-privileges networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} diff --git a/shared/services/rocketpool/assets/install/templates/prometheus.tmpl b/shared/services/rocketpool/assets/install/templates/prometheus.tmpl index dc5cfc13a..1a4591e3d 100644 --- a/shared/services/rocketpool/assets/install/templates/prometheus.tmpl +++ b/shared/services/rocketpool/assets/install/templates/prometheus.tmpl @@ -28,5 +28,6 @@ services: - "host.docker.internal:host-gateway" networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} volumes: prometheus-data: diff --git a/shared/services/rocketpool/assets/install/templates/validator.tmpl b/shared/services/rocketpool/assets/install/templates/validator.tmpl index 302bee54d..c37037f50 100644 --- a/shared/services/rocketpool/assets/install/templates/validator.tmpl +++ b/shared/services/rocketpool/assets/install/templates/validator.tmpl @@ -58,3 +58,4 @@ services: - no-new-privileges networks: net: + enable_ipv6: {{ .IsIPv6Enabled }} diff --git a/shared/services/rocketpool/assets/install/templates/watchtower.tmpl b/shared/services/rocketpool/assets/install/templates/watchtower.tmpl index e412411b7..06fa59472 100644 --- a/shared/services/rocketpool/assets/install/templates/watchtower.tmpl +++ b/shared/services/rocketpool/assets/install/templates/watchtower.tmpl @@ -25,3 +25,4 @@ services: - no-new-privileges networks: net: + enable_ipv6: {{ .IsIPv6Enabled }}