Setup Port Mapping

During the process of starting the node, after the node has been set up, it calls srv.setupPortMapping to set up port mapping.
Port mapping (or port forwarding) is a method used in networking to allow external devices to access services on a private network. It involves mapping a specific port on an external IP address (usually a public IP) to a specific port on an internal IP address (private IP) within a local network. Detailed introduction can be seen here.
Node needs to know the External IP and the mapped port, includes them in ENR and broadcast them to other nodes in the network, so that other nodes can send message to the NAT gateway and finally reach to the node. That’s what setupPortMapping handles.
notion image
 
The setupPortMapping function in the go-ethereum codebase initializes and manages port mappings for a server. The result of the setupPortMapping is the regular update of node’s ENR stored in srv.entries including External IP of the node’s gateway and the mapped port in gateway. So when the node broadcast the ENR which includes the corresponding gateway IP and mapped port to other nodes, other nodes can send message to the gateway which direct those messages to the node.
/// ---p2p/server.go--- // Start starts running the server. // Servers can not be re-used after stopping. func (srv *Server) Start() (err error) { // ... if err := srv.setupLocalNode(); err != nil { return err } srv.setupPortMapping() // ... }
 
setupPortMapping handles port mapping according to server’s NAT setting. srv.NAT is a interface helps manage how the node interacts with the router and configures port mappings in the gateway to ensure the node is accessible from outside its local network.
Step:
  1. Channel Initialization:
    1. A buffered channel srv.portMappingRegister is created to handle port mapping requests, like TCP and UDP.
  1. If No NAT Interface Configured(nil case):
    1. since no NAT interface is set up, geth doesn’t know anything about the NAT. It doesn't perform any operations, but just consume port mapping request.
  1. If External IP Specified (nat.ExtIP):
    1. nat.ExtIP is specified when the node has a known, fixed external IP address. This setup assumes that the local machine is directly reachable on this external IP address, potentially implying that either the node is not behind a typical NAT setup, or that the NAT configuration is such that the node's external IP and port mappings are static and do not require dynamic management.
  1. Default Case:
    1. For other NAT types (e.g., UPnP, NAT-PMP), the server starts a goroutine to manage the port mappings using the portMappingLoop function. srv.portMappingLoop is responsible for continuously managing and refreshing the port mappings.
/// ---p2p/server_nat.go--- type portMapping struct { protocol string name string port int // for use by the portMappingLoop goroutine: extPort int // the mapped port returned by the NAT interface nextTime mclock.AbsTime } // setupPortMapping starts the port mapping loop if necessary. // Note: this needs to be called after the LocalNode instance has been set on the server. func (srv *Server) setupPortMapping() { // portMappingRegister will receive up to two values: one for the TCP port if // listening is enabled, and one more for enabling UDP port mapping if discovery is // enabled. We make it buffered to avoid blocking setup while a mapping request is in // progress. srv.portMappingRegister = make(chan *portMapping, 2) switch srv.NAT.(type) { case nil: // No NAT interface configured. srv.loopWG.Add(1) go srv.consumePortMappingRequests() case nat.ExtIP: // ExtIP doesn't block, set the IP right away. ip, _ := srv.NAT.ExternalIP() srv.localnode.SetStaticIP(ip) srv.loopWG.Add(1) go srv.consumePortMappingRequests() default: srv.loopWG.Add(1) go srv.portMappingLoop() } } func (srv *Server) consumePortMappingRequests() { defer srv.loopWG.Done() for { select { case <-srv.quit: return case <-srv.portMappingRegister: } } }
 
portMappingLoop handles managing port mappings for both UDP and TCP protocols. This function ensures that the Ethereum node can be accessed from outside its local network by setting up and maintaining port mappings with the router. In the function, it basically periodically update gateway’s IP (External IP) and port mapping.
 
Why Need to Refresh Gateway External IP and Port Mapping?
  1. Network Changes:
      • Changes in the network, such as a new IP address being assigned to the router by the ISP (common in dynamic IP setups), might necessitate re-establishing port mappings. Refreshing the mappings ensures they remain valid despite such changes
  1. Dynamic Nature of NAT:
      • Many NAT devices (routers) have limited resources and might reclaim or reallocate port mappings if they are not refreshed. This is especially common in consumer-grade routers that might have a timeout period for port mappings.
 
Preparation Step:
  1. Deferred Wait Group Cleanup:
    1. The function begins by deferring the decrement of the wait group counter (srv.loopWG.Done()). This ensures that the wait group is properly managed when the loop exits.
  1. Logger Setup
    1. A helper function newLogger is defined to create a logger with specific fields (protocol, external port, internal port, and NAT interface).
  1. Variable Initialization
      • A map mappings is initialized to keep track of active port mappings.
      • Two alarms, refresh and extip, are created to handle scheduled tasks.
      • lastExtIP is initialized to store the last known external IP.
  1. Initial Scheduling
      • The extip alarm is scheduled to run immediately, so the External IP update will be done immediately.
  1. Deferred Schedule and Port Mapping Clean Up
    1. A deferred function ensures that the alarms are stopped and any existing port mappings in the gateway are deleted when the function exits.
Main Loop
The main loop runs indefinitely, handling various events such as quitting, external IP checks, and port mapping updates:
  1. Scheduling Refresh of Existing Mappings
    1. This loop schedules the next refresh time for each active mapping based on m.nextTime set in other branches.
  1. Quit
    1. If a quit signal is received, the loop exits.
  1. External IP Check
      • The external IP is checked periodically. Calls srv.NAT.ExternalIP to get current gateway IP.
      • If the external IP has changed or an error occurs, it updates lastExtIP and sets the static IP for the local node.
      • It then reschedules the port mappings to refresh immediately, this is because previous port mapping may be invalid when the gateway IP changed.
  1. Receiving Port Mapping Requests
      • New port mapping requests are added to the mappings map. (This happens in setupListening process)
      • The next refresh time is set to the current time, scheduling an immediate refresh.
  1. Refreshing Port Mappings
      • This block handles the periodic refresh of port mappings.
      • For each mapping, it attempts to refresh the port mapping in gateway by calling srv.NAT.AddMapping. Without extra setting(m.extPort), the external port equals the internal port.
      • If successful, it schedules next update time, and updates the local node's ENR with the new port information.
/// ---p2p/server_nat.go--- // portMappingLoop manages port mappings for UDP and TCP. func (srv *Server) portMappingLoop() { // Deferred Wait Group Cleanup defer srv.loopWG.Done() // Logger Setup: newLogger := func(p string, e int, i int) log.Logger { return log.New("proto", p, "extport", e, "intport", i, "interface", srv.NAT) } // Variable Initialization var ( mappings = make(map[string]*portMapping, 2) refresh = mclock.NewAlarm(srv.clock) // port mapping update schedule extip = mclock.NewAlarm(srv.clock) // external IP upate schedule lastExtIP net.IP ) // Initial Scheduling extip.Schedule(srv.clock.Now()) // Deferred Schedule and Port Mapping Clean Up defer func() { refresh.Stop() extip.Stop() for _, m := range mappings { if m.extPort != 0 { log := newLogger(m.protocol, m.extPort, m.port) log.Debug("Deleting port mapping") srv.NAT.DeleteMapping(m.protocol, m.extPort, m.port) } } }() for { // Scheduling Refresh of Existing Mappings Based on m.nextTime for _, m := range mappings { refresh.Schedule(m.nextTime) } select { // Quit case <-srv.quit: return // External IP Check case <-extip.C(): extip.Schedule(srv.clock.Now().Add(extipRetryInterval)) ip, err := srv.NAT.ExternalIP() if err != nil { log.Debug("Couldn't get external IP", "err", err, "interface", srv.NAT) } else if !ip.Equal(lastExtIP) { log.Debug("External IP changed", "ip", extip, "interface", srv.NAT) } else { continue } // Here, we either failed to get the external IP, or it has changed. lastExtIP = ip srv.localnode.SetStaticIP(ip) // Ensure port mappings are refreshed in case we have moved to a new network. for _, m := range mappings { m.nextTime = srv.clock.Now() } // Receiving Port Mapping Requests case m := <-srv.portMappingRegister: if m.protocol != "TCP" && m.protocol != "UDP" { panic("unknown NAT protocol name: " + m.protocol) } mappings[m.protocol] = m m.nextTime = srv.clock.Now() // Refreshing Port Mappings case <-refresh.C(): for _, m := range mappings { if srv.clock.Now() < m.nextTime { continue } external := m.port if m.extPort != 0 { external = m.extPort } log := newLogger(m.protocol, external, m.port) log.Trace("Attempting port mapping") p, err := srv.NAT.AddMapping(m.protocol, external, m.port, m.name, portMapDuration) if err != nil { log.Debug("Couldn't add port mapping", "err", err) m.extPort = 0 m.nextTime = srv.clock.Now().Add(portMapRetryInterval) continue } // It was mapped! m.extPort = int(p) m.nextTime = srv.clock.Now().Add(portMapRefreshInterval) if external != m.extPort { log = newLogger(m.protocol, m.extPort, m.port) log.Info("NAT mapped alternative port") } else { log.Info("NAT mapped port") } // Update port in local ENR. switch m.protocol { case "TCP": srv.localnode.Set(enr.TCP(m.extPort)) case "UDP": srv.localnode.SetFallbackUDP(m.extPort) } } } } }