github.com/ethereum/go-ethereum@v1.16.1/p2p/nat/stun.go (about)

     1  // Copyright 2025 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package nat
    18  
    19  import (
    20  	_ "embed"
    21  	"errors"
    22  	"fmt"
    23  	"math/rand"
    24  	"net"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/ethereum/go-ethereum/log"
    29  	stunV2 "github.com/pion/stun/v2"
    30  )
    31  
    32  //go:embed stun-list.txt
    33  var stunDefaultServers string
    34  
    35  const requestLimit = 3
    36  
    37  var errSTUNFailed = errors.New("STUN requests failed")
    38  
    39  type stun struct {
    40  	serverList []string
    41  }
    42  
    43  func newSTUN(serverAddr string) (Interface, error) {
    44  	s := new(stun)
    45  	if serverAddr == "" {
    46  		s.serverList = strings.Split(stunDefaultServers, "\n")
    47  	} else {
    48  		_, err := net.ResolveUDPAddr("udp4", serverAddr)
    49  		if err != nil {
    50  			return nil, err
    51  		}
    52  		s.serverList = []string{serverAddr}
    53  	}
    54  	return s, nil
    55  }
    56  
    57  func (s stun) String() string {
    58  	if len(s.serverList) == 1 {
    59  		return fmt.Sprintf("stun:%s", s.serverList[0])
    60  	}
    61  	return "stun"
    62  }
    63  
    64  func (s stun) MarshalText() ([]byte, error) {
    65  	return []byte(s.String()), nil
    66  }
    67  
    68  func (stun) SupportsMapping() bool {
    69  	return false
    70  }
    71  
    72  func (stun) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) (uint16, error) {
    73  	return uint16(extport), nil
    74  }
    75  
    76  func (stun) DeleteMapping(string, int, int) error {
    77  	return nil
    78  }
    79  
    80  func (s *stun) ExternalIP() (net.IP, error) {
    81  	for _, server := range s.randomServers(requestLimit) {
    82  		ip, err := s.externalIP(server)
    83  		if err != nil {
    84  			log.Debug("STUN request failed", "server", server, "err", err)
    85  			continue
    86  		}
    87  		return ip, nil
    88  	}
    89  	return nil, errSTUNFailed
    90  }
    91  
    92  func (s *stun) randomServers(n int) []string {
    93  	n = min(n, len(s.serverList))
    94  	m := make(map[int]struct{}, n)
    95  	list := make([]string, 0, n)
    96  	for i := 0; i < len(s.serverList)*2 && len(list) < n; i++ {
    97  		index := rand.Intn(len(s.serverList))
    98  		if _, alreadyHit := m[index]; alreadyHit {
    99  			continue
   100  		}
   101  		list = append(list, s.serverList[index])
   102  		m[index] = struct{}{}
   103  	}
   104  	return list
   105  }
   106  
   107  func (s *stun) externalIP(server string) (net.IP, error) {
   108  	_, _, err := net.SplitHostPort(server)
   109  	if err != nil {
   110  		server += fmt.Sprintf(":%d", stunV2.DefaultPort)
   111  	}
   112  
   113  	log.Trace("Attempting STUN binding request", "server", server)
   114  	conn, err := stunV2.Dial("udp4", server)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	defer conn.Close()
   119  
   120  	message, err := stunV2.Build(stunV2.TransactionID, stunV2.BindingRequest)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	var responseError error
   126  	var mappedAddr stunV2.XORMappedAddress
   127  	err = conn.Do(message, func(event stunV2.Event) {
   128  		if event.Error != nil {
   129  			responseError = event.Error
   130  			return
   131  		}
   132  		if err := mappedAddr.GetFrom(event.Message); err != nil {
   133  			responseError = err
   134  		}
   135  	})
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	if responseError != nil {
   140  		return nil, responseError
   141  	}
   142  	log.Trace("STUN returned IP", "server", server, "ip", mappedAddr.IP)
   143  	return mappedAddr.IP, nil
   144  }