From 218b697f053b3e09c4a60d27feeb32af30acab58 Mon Sep 17 00:00:00 2001
From: Felix Lange <fjl@twurst.com>
Date: Wed, 22 Jan 2025 09:29:34 +0100
Subject: [PATCH] p2p: support configuring NAT in TOML file (#31041)

This is an alternative for #27407 with a solution based on gencodec.
With the PR, one can now configure like this:

```
# config.toml
[Node.P2P]
NAT = "extip:33.33.33.33"
```

```shell
$ geth --config config.toml
...
INFO [01-17|16:37:31.436] Started P2P networking      self=enode://2290...ab@33.33.33.33:30303
```
---
 go.mod             |  14 ++--
 go.sum             |  24 +++----
 p2p/config.go      | 144 +++++++++++++++++++++++++++++++++++++++
 p2p/config_toml.go | 165 +++++++++++++++++++++++++++++++++++++++++++++
 p2p/nat/nat.go     |  15 +++--
 p2p/nat/natpmp.go  |   4 ++
 p2p/server.go      |  99 ---------------------------
 7 files changed, 342 insertions(+), 123 deletions(-)
 create mode 100644 p2p/config.go
 create mode 100644 p2p/config_toml.go

diff --git a/go.mod b/go.mod
index 20526956df..51f9f1bda7 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/ethereum/go-ethereum
 
-go 1.22
+go 1.22.0
 
 require (
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0
@@ -25,7 +25,7 @@ require (
 	github.com/ethereum/go-verkle v0.2.2
 	github.com/fatih/color v1.16.0
 	github.com/ferranbt/fastssz v0.1.2
-	github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e
+	github.com/fjl/gencodec v0.0.0-20250117152317-bc3e1c7619d4
 	github.com/fsnotify/fsnotify v1.6.0
 	github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
 	github.com/gofrs/flock v0.8.1
@@ -62,13 +62,13 @@ require (
 	github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
 	github.com/urfave/cli/v2 v2.25.7
 	go.uber.org/automaxprocs v1.5.2
-	golang.org/x/crypto v0.31.0
+	golang.org/x/crypto v0.32.0
 	golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
 	golang.org/x/sync v0.10.0
-	golang.org/x/sys v0.28.0
+	golang.org/x/sys v0.29.0
 	golang.org/x/text v0.21.0
 	golang.org/x/time v0.5.0
-	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
+	golang.org/x/tools v0.29.0
 	google.golang.org/protobuf v1.34.2
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/yaml.v3 v3.0.1
@@ -138,8 +138,8 @@ require (
 	github.com/tklauser/go-sysconf v0.3.12 // indirect
 	github.com/tklauser/numcpus v0.6.1 // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
-	golang.org/x/mod v0.17.0 // indirect
-	golang.org/x/net v0.25.0 // indirect
+	golang.org/x/mod v0.22.0 // indirect
+	golang.org/x/net v0.34.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	rsc.io/tmplfunc v0.0.3 // indirect
 )
diff --git a/go.sum b/go.sum
index ded07f4867..8a280398d0 100644
--- a/go.sum
+++ b/go.sum
@@ -172,8 +172,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/ferranbt/fastssz v0.1.2 h1:Dky6dXlngF6Qjc+EfDipAkE83N5I5DE68bY6O0VLNPk=
 github.com/ferranbt/fastssz v0.1.2/go.mod h1:X5UPrE2u1UJjxHA8X54u04SBwdAQjG2sFtWs39YxyWs=
-github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e h1:bBLctRc7kr01YGvaDfgLbTwjFNW5jdp5y5rj8XXBHfY=
-github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY=
+github.com/fjl/gencodec v0.0.0-20250117152317-bc3e1c7619d4 h1:HhaRPDFEB1BrvBF+UCUnhyY9y5dtuBxy0myn+7sFjAw=
+github.com/fjl/gencodec v0.0.0-20250117152317-bc3e1c7619d4/go.mod h1:Um1dFHPONZGTHog1qD1NaWjXJW/SPB38wPv0O8uZ2fI=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -528,8 +528,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -563,8 +563,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -601,8 +601,8 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -684,8 +684,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 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=
@@ -750,8 +750,8 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
+golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/p2p/config.go b/p2p/config.go
new file mode 100644
index 0000000000..14492a2e55
--- /dev/null
+++ b/p2p/config.go
@@ -0,0 +1,144 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+package p2p
+
+import (
+	"crypto/ecdsa"
+	"fmt"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/nat"
+	"github.com/ethereum/go-ethereum/p2p/netutil"
+)
+
+//go:generate go run github.com/fjl/gencodec -type Config -field-override configMarshaling -formats toml -out config_toml.go
+
+// Config holds Server options.
+type Config struct {
+	// This field must be set to a valid secp256k1 private key.
+	PrivateKey *ecdsa.PrivateKey `toml:"-"`
+
+	// MaxPeers is the maximum number of peers that can be
+	// connected. It must be greater than zero.
+	MaxPeers int
+
+	// MaxPendingPeers is the maximum number of peers that can be pending in the
+	// handshake phase, counted separately for inbound and outbound connections.
+	// Zero defaults to preset values.
+	MaxPendingPeers int `toml:",omitempty"`
+
+	// DialRatio controls the ratio of inbound to dialed connections.
+	// Example: a DialRatio of 2 allows 1/2 of connections to be dialed.
+	// Setting DialRatio to zero defaults it to 3.
+	DialRatio int `toml:",omitempty"`
+
+	// NoDiscovery can be used to disable the peer discovery mechanism.
+	// Disabling is useful for protocol debugging (manual topology).
+	NoDiscovery bool
+
+	// DiscoveryV4 specifies whether V4 discovery should be started.
+	DiscoveryV4 bool `toml:",omitempty"`
+
+	// DiscoveryV5 specifies whether the new topic-discovery based V5 discovery
+	// protocol should be started or not.
+	DiscoveryV5 bool `toml:",omitempty"`
+
+	// Name sets the node name of this server.
+	Name string `toml:"-"`
+
+	// BootstrapNodes are used to establish connectivity
+	// with the rest of the network.
+	BootstrapNodes []*enode.Node
+
+	// BootstrapNodesV5 are used to establish connectivity
+	// with the rest of the network using the V5 discovery
+	// protocol.
+	BootstrapNodesV5 []*enode.Node `toml:",omitempty"`
+
+	// Static nodes are used as pre-configured connections which are always
+	// maintained and re-connected on disconnects.
+	StaticNodes []*enode.Node
+
+	// Trusted nodes are used as pre-configured connections which are always
+	// allowed to connect, even above the peer limit.
+	TrustedNodes []*enode.Node
+
+	// Connectivity can be restricted to certain IP networks.
+	// If this option is set to a non-nil value, only hosts which match one of the
+	// IP networks contained in the list are considered.
+	NetRestrict *netutil.Netlist `toml:",omitempty"`
+
+	// NodeDatabase is the path to the database containing the previously seen
+	// live nodes in the network.
+	NodeDatabase string `toml:",omitempty"`
+
+	// Protocols should contain the protocols supported
+	// by the server. Matching protocols are launched for
+	// each peer.
+	Protocols []Protocol `toml:"-" json:"-"`
+
+	// If ListenAddr is set to a non-nil address, the server
+	// will listen for incoming connections.
+	//
+	// If the port is zero, the operating system will pick a port. The
+	// ListenAddr field will be updated with the actual address when
+	// the server is started.
+	ListenAddr string
+
+	// If DiscAddr is set to a non-nil value, the server will use ListenAddr
+	// for TCP and DiscAddr for the UDP discovery protocol.
+	DiscAddr string
+
+	// If set to a non-nil value, the given NAT port mapper
+	// is used to make the listening port available to the
+	// Internet.
+	NAT nat.Interface `toml:",omitempty"`
+
+	// If Dialer is set to a non-nil value, the given Dialer
+	// is used to dial outbound peer connections.
+	Dialer NodeDialer `toml:"-"`
+
+	// If NoDial is true, the server will not dial any peers.
+	NoDial bool `toml:",omitempty"`
+
+	// If EnableMsgEvents is set then the server will emit PeerEvents
+	// whenever a message is sent to or received from a peer
+	EnableMsgEvents bool
+
+	// Logger is a custom logger to use with the p2p.Server.
+	Logger log.Logger `toml:"-"`
+
+	clock mclock.Clock
+}
+
+type configMarshaling struct {
+	NAT configNAT
+}
+
+type configNAT struct {
+	nat.Interface
+}
+
+func (w *configNAT) UnmarshalText(input []byte) error {
+	n, err := nat.Parse(string(input))
+	if err != nil {
+		return fmt.Errorf("invalid NAT specification: %v", err)
+	}
+	w.Interface = n
+	return nil
+}
diff --git a/p2p/config_toml.go b/p2p/config_toml.go
new file mode 100644
index 0000000000..8a255e62fb
--- /dev/null
+++ b/p2p/config_toml.go
@@ -0,0 +1,165 @@
+// Code generated by github.com/fjl/gencodec. DO NOT EDIT.
+
+package p2p
+
+import (
+	"crypto/ecdsa"
+
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/nat"
+	"github.com/ethereum/go-ethereum/p2p/netutil"
+)
+
+var _ = (*configMarshaling)(nil)
+
+// MarshalTOML marshals as TOML.
+func (c Config) MarshalTOML() (interface{}, error) {
+	type Config struct {
+		PrivateKey       *ecdsa.PrivateKey `toml:"-"`
+		MaxPeers         int
+		MaxPendingPeers  int `toml:",omitempty"`
+		DialRatio        int `toml:",omitempty"`
+		NoDiscovery      bool
+		DiscoveryV4      bool   `toml:",omitempty"`
+		DiscoveryV5      bool   `toml:",omitempty"`
+		Name             string `toml:"-"`
+		BootstrapNodes   []*enode.Node
+		BootstrapNodesV5 []*enode.Node `toml:",omitempty"`
+		StaticNodes      []*enode.Node
+		TrustedNodes     []*enode.Node
+		NetRestrict      *netutil.Netlist `toml:",omitempty"`
+		NodeDatabase     string           `toml:",omitempty"`
+		Protocols        []Protocol       `toml:"-" json:"-"`
+		ListenAddr       string
+		DiscAddr         string
+		NAT              nat.Interface `toml:",omitempty"`
+		Dialer           NodeDialer    `toml:"-"`
+		NoDial           bool          `toml:",omitempty"`
+		EnableMsgEvents  bool
+		Logger           log.Logger `toml:"-"`
+	}
+	var enc Config
+	enc.PrivateKey = c.PrivateKey
+	enc.MaxPeers = c.MaxPeers
+	enc.MaxPendingPeers = c.MaxPendingPeers
+	enc.DialRatio = c.DialRatio
+	enc.NoDiscovery = c.NoDiscovery
+	enc.DiscoveryV4 = c.DiscoveryV4
+	enc.DiscoveryV5 = c.DiscoveryV5
+	enc.Name = c.Name
+	enc.BootstrapNodes = c.BootstrapNodes
+	enc.BootstrapNodesV5 = c.BootstrapNodesV5
+	enc.StaticNodes = c.StaticNodes
+	enc.TrustedNodes = c.TrustedNodes
+	enc.NetRestrict = c.NetRestrict
+	enc.NodeDatabase = c.NodeDatabase
+	enc.Protocols = c.Protocols
+	enc.ListenAddr = c.ListenAddr
+	enc.DiscAddr = c.DiscAddr
+	enc.NAT = c.NAT
+	enc.Dialer = c.Dialer
+	enc.NoDial = c.NoDial
+	enc.EnableMsgEvents = c.EnableMsgEvents
+	enc.Logger = c.Logger
+	return &enc, nil
+}
+
+// UnmarshalTOML unmarshals from TOML.
+func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
+	type Config struct {
+		PrivateKey       *ecdsa.PrivateKey `toml:"-"`
+		MaxPeers         *int
+		MaxPendingPeers  *int `toml:",omitempty"`
+		DialRatio        *int `toml:",omitempty"`
+		NoDiscovery      *bool
+		DiscoveryV4      *bool   `toml:",omitempty"`
+		DiscoveryV5      *bool   `toml:",omitempty"`
+		Name             *string `toml:"-"`
+		BootstrapNodes   []*enode.Node
+		BootstrapNodesV5 []*enode.Node `toml:",omitempty"`
+		StaticNodes      []*enode.Node
+		TrustedNodes     []*enode.Node
+		NetRestrict      *netutil.Netlist `toml:",omitempty"`
+		NodeDatabase     *string          `toml:",omitempty"`
+		Protocols        []Protocol       `toml:"-" json:"-"`
+		ListenAddr       *string
+		DiscAddr         *string
+		NAT              *configNAT `toml:",omitempty"`
+		Dialer           NodeDialer `toml:"-"`
+		NoDial           *bool      `toml:",omitempty"`
+		EnableMsgEvents  *bool
+		Logger           log.Logger `toml:"-"`
+	}
+	var dec Config
+	if err := unmarshal(&dec); err != nil {
+		return err
+	}
+	if dec.PrivateKey != nil {
+		c.PrivateKey = dec.PrivateKey
+	}
+	if dec.MaxPeers != nil {
+		c.MaxPeers = *dec.MaxPeers
+	}
+	if dec.MaxPendingPeers != nil {
+		c.MaxPendingPeers = *dec.MaxPendingPeers
+	}
+	if dec.DialRatio != nil {
+		c.DialRatio = *dec.DialRatio
+	}
+	if dec.NoDiscovery != nil {
+		c.NoDiscovery = *dec.NoDiscovery
+	}
+	if dec.DiscoveryV4 != nil {
+		c.DiscoveryV4 = *dec.DiscoveryV4
+	}
+	if dec.DiscoveryV5 != nil {
+		c.DiscoveryV5 = *dec.DiscoveryV5
+	}
+	if dec.Name != nil {
+		c.Name = *dec.Name
+	}
+	if dec.BootstrapNodes != nil {
+		c.BootstrapNodes = dec.BootstrapNodes
+	}
+	if dec.BootstrapNodesV5 != nil {
+		c.BootstrapNodesV5 = dec.BootstrapNodesV5
+	}
+	if dec.StaticNodes != nil {
+		c.StaticNodes = dec.StaticNodes
+	}
+	if dec.TrustedNodes != nil {
+		c.TrustedNodes = dec.TrustedNodes
+	}
+	if dec.NetRestrict != nil {
+		c.NetRestrict = dec.NetRestrict
+	}
+	if dec.NodeDatabase != nil {
+		c.NodeDatabase = *dec.NodeDatabase
+	}
+	if dec.Protocols != nil {
+		c.Protocols = dec.Protocols
+	}
+	if dec.ListenAddr != nil {
+		c.ListenAddr = *dec.ListenAddr
+	}
+	if dec.DiscAddr != nil {
+		c.DiscAddr = *dec.DiscAddr
+	}
+	if dec.NAT != nil {
+		c.NAT = dec.NAT
+	}
+	if dec.Dialer != nil {
+		c.Dialer = dec.Dialer
+	}
+	if dec.NoDial != nil {
+		c.NoDial = *dec.NoDial
+	}
+	if dec.EnableMsgEvents != nil {
+		c.EnableMsgEvents = *dec.EnableMsgEvents
+	}
+	if dec.Logger != nil {
+		c.Logger = dec.Logger
+	}
+	return nil
+}
diff --git a/p2p/nat/nat.go b/p2p/nat/nat.go
index c656044268..f7fe8196c9 100644
--- a/p2p/nat/nat.go
+++ b/p2p/nat/nat.go
@@ -133,8 +133,9 @@ func Map(m Interface, c <-chan struct{}, protocol string, extport, intport int,
 // Mapping operations will not return an error but won't actually do anything.
 type ExtIP net.IP
 
-func (n ExtIP) ExternalIP() (net.IP, error) { return net.IP(n), nil }
-func (n ExtIP) String() string              { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) }
+func (n ExtIP) ExternalIP() (net.IP, error)  { return net.IP(n), nil }
+func (n ExtIP) String() string               { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) }
+func (n ExtIP) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("extip:%v", net.IP(n))), nil }
 
 // These do nothing.
 
@@ -148,7 +149,7 @@ func (ExtIP) DeleteMapping(string, int, int) error { return nil }
 func Any() Interface {
 	// TODO: attempt to discover whether the local machine has an
 	// Internet-class address. Return ExtIP in this case.
-	return startautodisc("UPnP or NAT-PMP", func() Interface {
+	return startautodisc("any", func() Interface {
 		found := make(chan Interface, 2)
 		go func() { found <- discoverUPnP() }()
 		go func() { found <- discoverPMP() }()
@@ -164,7 +165,7 @@ func Any() Interface {
 // UPnP returns a port mapper that uses UPnP. It will attempt to
 // discover the address of your router using UDP broadcasts.
 func UPnP() Interface {
-	return startautodisc("UPnP", discoverUPnP)
+	return startautodisc("upnp", discoverUPnP)
 }
 
 // PMP returns a port mapper that uses NAT-PMP. The provided gateway
@@ -174,7 +175,7 @@ func PMP(gateway net.IP) Interface {
 	if gateway != nil {
 		return &pmp{gw: gateway, c: natpmp.NewClient(gateway)}
 	}
-	return startautodisc("NAT-PMP", discoverPMP)
+	return startautodisc("natpmp", discoverPMP)
 }
 
 // autodisc represents a port mapping mechanism that is still being
@@ -228,6 +229,10 @@ func (n *autodisc) String() string {
 	return n.found.String()
 }
 
+func (n *autodisc) MarshalText() ([]byte, error) {
+	return []byte(n.what), nil
+}
+
 // wait blocks until auto-discovery has been performed.
 func (n *autodisc) wait() error {
 	n.once.Do(func() {
diff --git a/p2p/nat/natpmp.go b/p2p/nat/natpmp.go
index 94ef1f4b68..b8f59ee890 100644
--- a/p2p/nat/natpmp.go
+++ b/p2p/nat/natpmp.go
@@ -70,6 +70,10 @@ func (n *pmp) DeleteMapping(protocol string, extport, intport int) (err error) {
 	return err
 }
 
+func (n *pmp) MarshalText() ([]byte, error) {
+	return []byte(fmt.Sprintf("natpmp:%v", n.gw)), nil
+}
+
 func discoverPMP() Interface {
 	// run external address lookups on all potential gateways
 	gws := potentialGateways()
diff --git a/p2p/server.go b/p2p/server.go
index 172f0667eb..c1564352e5 100644
--- a/p2p/server.go
+++ b/p2p/server.go
@@ -39,7 +39,6 @@ import (
 	"github.com/ethereum/go-ethereum/p2p/discover"
 	"github.com/ethereum/go-ethereum/p2p/enode"
 	"github.com/ethereum/go-ethereum/p2p/enr"
-	"github.com/ethereum/go-ethereum/p2p/nat"
 	"github.com/ethereum/go-ethereum/p2p/netutil"
 )
 
@@ -72,104 +71,6 @@ var (
 	errProtoHandshakeError = errors.New("rlpx proto error")
 )
 
-// Config holds Server options.
-type Config struct {
-	// This field must be set to a valid secp256k1 private key.
-	PrivateKey *ecdsa.PrivateKey `toml:"-"`
-
-	// MaxPeers is the maximum number of peers that can be
-	// connected. It must be greater than zero.
-	MaxPeers int
-
-	// MaxPendingPeers is the maximum number of peers that can be pending in the
-	// handshake phase, counted separately for inbound and outbound connections.
-	// Zero defaults to preset values.
-	MaxPendingPeers int `toml:",omitempty"`
-
-	// DialRatio controls the ratio of inbound to dialed connections.
-	// Example: a DialRatio of 2 allows 1/2 of connections to be dialed.
-	// Setting DialRatio to zero defaults it to 3.
-	DialRatio int `toml:",omitempty"`
-
-	// NoDiscovery can be used to disable the peer discovery mechanism.
-	// Disabling is useful for protocol debugging (manual topology).
-	NoDiscovery bool
-
-	// DiscoveryV4 specifies whether V4 discovery should be started.
-	DiscoveryV4 bool `toml:",omitempty"`
-
-	// DiscoveryV5 specifies whether the new topic-discovery based V5 discovery
-	// protocol should be started or not.
-	DiscoveryV5 bool `toml:",omitempty"`
-
-	// Name sets the node name of this server.
-	Name string `toml:"-"`
-
-	// BootstrapNodes are used to establish connectivity
-	// with the rest of the network.
-	BootstrapNodes []*enode.Node
-
-	// BootstrapNodesV5 are used to establish connectivity
-	// with the rest of the network using the V5 discovery
-	// protocol.
-	BootstrapNodesV5 []*enode.Node `toml:",omitempty"`
-
-	// Static nodes are used as pre-configured connections which are always
-	// maintained and re-connected on disconnects.
-	StaticNodes []*enode.Node
-
-	// Trusted nodes are used as pre-configured connections which are always
-	// allowed to connect, even above the peer limit.
-	TrustedNodes []*enode.Node
-
-	// Connectivity can be restricted to certain IP networks.
-	// If this option is set to a non-nil value, only hosts which match one of the
-	// IP networks contained in the list are considered.
-	NetRestrict *netutil.Netlist `toml:",omitempty"`
-
-	// NodeDatabase is the path to the database containing the previously seen
-	// live nodes in the network.
-	NodeDatabase string `toml:",omitempty"`
-
-	// Protocols should contain the protocols supported
-	// by the server. Matching protocols are launched for
-	// each peer.
-	Protocols []Protocol `toml:"-" json:"-"`
-
-	// If ListenAddr is set to a non-nil address, the server
-	// will listen for incoming connections.
-	//
-	// If the port is zero, the operating system will pick a port. The
-	// ListenAddr field will be updated with the actual address when
-	// the server is started.
-	ListenAddr string
-
-	// If DiscAddr is set to a non-nil value, the server will use ListenAddr
-	// for TCP and DiscAddr for the UDP discovery protocol.
-	DiscAddr string
-
-	// If set to a non-nil value, the given NAT port mapper
-	// is used to make the listening port available to the
-	// Internet.
-	NAT nat.Interface `toml:",omitempty"`
-
-	// If Dialer is set to a non-nil value, the given Dialer
-	// is used to dial outbound peer connections.
-	Dialer NodeDialer `toml:"-"`
-
-	// If NoDial is true, the server will not dial any peers.
-	NoDial bool `toml:",omitempty"`
-
-	// If EnableMsgEvents is set then the server will emit PeerEvents
-	// whenever a message is sent to or received from a peer
-	EnableMsgEvents bool
-
-	// Logger is a custom logger to use with the p2p.Server.
-	Logger log.Logger `toml:",omitempty"`
-
-	clock mclock.Clock
-}
-
 // Server manages all peer connections.
 type Server struct {
 	// Config fields may not be modified while the server is running.