github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/core/ip/resolver.go (about)

     1  /*
     2   * Copyright (C) 2017 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU 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   * This program 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 General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package ip
    19  
    20  import (
    21  	"net"
    22  	"sync"
    23  
    24  	"github.com/pkg/errors"
    25  	"github.com/rs/zerolog/log"
    26  
    27  	"github.com/mysteriumnetwork/node/requests"
    28  )
    29  
    30  const apiClient = "goclient-v0.1"
    31  
    32  // Resolver allows resolving current public and outbound IPs
    33  type Resolver interface {
    34  	GetOutboundIP() (string, error)
    35  	GetPublicIP() (string, error)
    36  	GetProxyIP(proxyPort int) (string, error)
    37  }
    38  
    39  // ResolverImpl represents data required to operate resolving
    40  type ResolverImpl struct {
    41  	bindAddress string
    42  	url         string
    43  	httpClient  *requests.HTTPClient
    44  	fallbacks   []string
    45  }
    46  
    47  // NewResolver creates new ip-detector resolver with default timeout of one minute
    48  func NewResolver(httpClient *requests.HTTPClient, bindAddress, url string, fallbacks []string) *ResolverImpl {
    49  	return &ResolverImpl{
    50  		bindAddress: bindAddress,
    51  		url:         url,
    52  		httpClient:  httpClient,
    53  		fallbacks:   fallbacks,
    54  	}
    55  }
    56  
    57  type ipResponse struct {
    58  	IP string `json:"IP"`
    59  }
    60  
    61  // declared as var for override in test
    62  var checkAddress = "8.8.8.8:53"
    63  
    64  // GetOutboundIP returns current outbound IP as string for current system
    65  func (r *ResolverImpl) GetOutboundIP() (string, error) {
    66  	ip, err := r.getOutboundIP()
    67  	if err != nil {
    68  		return "", nil
    69  	}
    70  	return ip.String(), nil
    71  }
    72  
    73  func (r *ResolverImpl) getOutboundIP() (net.IP, error) {
    74  	ipAddress := net.ParseIP(r.bindAddress)
    75  	localIPAddress := net.UDPAddr{IP: ipAddress}
    76  
    77  	dialer := net.Dialer{LocalAddr: &localIPAddress}
    78  
    79  	conn, err := dialer.Dial("udp4", checkAddress)
    80  	if err != nil {
    81  		return nil, errors.Wrap(err, "failed to determine outbound IP")
    82  	}
    83  	defer conn.Close()
    84  
    85  	return conn.LocalAddr().(*net.UDPAddr).IP, nil
    86  }
    87  
    88  // GetPublicIP returns current public IP
    89  func (r *ResolverImpl) GetPublicIP() (string, error) {
    90  	var ipResponse ipResponse
    91  
    92  	request, err := requests.NewGetRequest(r.url, "", nil)
    93  	if err != nil {
    94  		log.Error().Err(err).Msg("")
    95  		return "", err
    96  	}
    97  	request.Header.Set("User-Agent", apiClient)
    98  	request.Header.Set("Accept", "application/json")
    99  
   100  	err = r.httpClient.DoRequestAndParseResponse(request, &ipResponse)
   101  	if err != nil {
   102  		log.Err(err).Msg("could not reach location service, will use fallbacks")
   103  		return r.findPublicIPViaFallbacks()
   104  	}
   105  
   106  	return ipResponse.IP, nil
   107  }
   108  
   109  // GetProxyIP returns proxy public IP
   110  func (r *ResolverImpl) GetProxyIP(proxyPort int) (string, error) {
   111  	var ipResponse ipResponse
   112  
   113  	request, err := requests.NewGetRequest(r.url, "", nil)
   114  	if err != nil {
   115  		log.Error().Err(err).Msg("")
   116  		return "", err
   117  	}
   118  
   119  	request.Header.Set("User-Agent", apiClient)
   120  	request.Header.Set("Accept", "application/json")
   121  
   122  	err = r.httpClient.DoRequestViaProxyAndParseResponse(request, &ipResponse, proxyPort)
   123  	if err != nil {
   124  		log.Err(err).Msg("could not reach location service, will use fallbacks")
   125  		return r.findPublicIPViaFallbacks()
   126  	}
   127  
   128  	return ipResponse.IP, nil
   129  }
   130  
   131  func (r *ResolverImpl) findPublicIPViaFallbacks() (string, error) {
   132  	// To prevent blocking for a long time on a service that might be dead, use the following fallback mechanic:
   133  	// Choose 3 fallback addresses at random and execute lookups on them in parallel.
   134  	// Return the first successful result or an error if such occurs.
   135  	// This prevents providers from not being able to provide sessions due to not having a fresh public IP address.
   136  	desiredLength := 3
   137  	res := make(chan string, desiredLength)
   138  	wg := sync.WaitGroup{}
   139  	wg.Add(desiredLength)
   140  
   141  	go func() {
   142  		wg.Wait()
   143  		close(res)
   144  	}()
   145  
   146  	for _, v := range shuffleStringSlice(r.fallbacks)[:desiredLength] {
   147  		go func(url string) {
   148  			defer wg.Done()
   149  			r, err := RequestAndParsePlainIPResponse(r.httpClient, url)
   150  			if err != nil {
   151  				log.Err(err).Str("url", url).Msg("public ip fallback error")
   152  			}
   153  			res <- r
   154  		}(v)
   155  	}
   156  
   157  	for ip := range res {
   158  		if ip != "" {
   159  			return ip, nil
   160  		}
   161  	}
   162  
   163  	return "", errors.New("out of fallbacks")
   164  }