git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cloudflare/ips.go (about)

     1  package cloudflare
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net"
     9  	"net/http"
    10  	"sync"
    11  )
    12  
    13  //go:embed ips.json
    14  var IpsJson []byte
    15  
    16  var cloudflareIps *cloudflareIpsRanges
    17  var cloudflareIpsMutex sync.RWMutex
    18  
    19  type cloudflareIpsRanges struct {
    20  	Ipv4 []net.IPNet
    21  	Ipv6 []net.IPNet
    22  }
    23  
    24  // CustomHostnameOwnershipVerificationHTTP represents a response from the Custom Hostnames endpoints.
    25  type CloudflareIpsResult struct {
    26  	Ipv4Cidrs []string `json:"ipv4_cidrs"`
    27  	Ipv6Cidrs []string `json:"ipv6_cidrs"`
    28  	Etag      string   `json:"etag"`
    29  }
    30  
    31  // TODO: return error?
    32  func IsCloudflareIP(ipAddress string) (ret bool, err error) {
    33  	if cloudflareIps == nil {
    34  		err = loadCloudflareIps()
    35  		if err != nil {
    36  			return
    37  		}
    38  	}
    39  
    40  	ip := net.ParseIP(ipAddress)
    41  	if ip == nil {
    42  		err = fmt.Errorf("IP address is not valid: %s", ipAddress)
    43  		return
    44  	}
    45  
    46  	cloudflareIpsMutex.RLock()
    47  	defer cloudflareIpsMutex.RUnlock()
    48  
    49  	// Is IPv4
    50  	var networks []net.IPNet
    51  	if ip.To4() != nil {
    52  		networks = cloudflareIps.Ipv4
    53  	} else {
    54  		networks = cloudflareIps.Ipv6
    55  	}
    56  
    57  	for _, network := range networks {
    58  		if network.Contains(ip) {
    59  			ret = true
    60  			return
    61  		}
    62  	}
    63  
    64  	return
    65  }
    66  
    67  func loadCloudflareIps() (err error) {
    68  	var cloudflareIpsData CloudflareIpsResult
    69  
    70  	err = json.Unmarshal(IpsJson, &cloudflareIpsData)
    71  	if err != nil {
    72  		err = fmt.Errorf("cloudflare: Error parsing Cloduflare IPs ranges: %w", err)
    73  		return
    74  	}
    75  
    76  	ips := cloudflareIpsRanges{
    77  		Ipv4: make([]net.IPNet, len(cloudflareIpsData.Ipv4Cidrs)),
    78  		Ipv6: make([]net.IPNet, len(cloudflareIpsData.Ipv6Cidrs)),
    79  	}
    80  
    81  	for i, cidr := range cloudflareIpsData.Ipv4Cidrs {
    82  		var network *net.IPNet
    83  		_, network, err = net.ParseCIDR(cidr)
    84  		if err != nil {
    85  			err = fmt.Errorf("cloudflare: CIDR (%s) is not valid: %w", cidr, err)
    86  			return
    87  		}
    88  		ips.Ipv4[i] = *network
    89  	}
    90  
    91  	for i, cidr := range cloudflareIpsData.Ipv6Cidrs {
    92  		var network *net.IPNet
    93  		_, network, err = net.ParseCIDR(cidr)
    94  		if err != nil {
    95  			err = fmt.Errorf("cloudflare: CIDR (%s) is not valid: %w", cidr, err)
    96  			return
    97  		}
    98  		ips.Ipv6[i] = *network
    99  	}
   100  
   101  	cloudflareIpsMutex.Lock()
   102  	cloudflareIps = &ips
   103  	cloudflareIpsMutex.Unlock()
   104  	return
   105  }
   106  
   107  // Fetch the IPs used on the Cloudflare network
   108  // Ips are fetched from https://api.cloudflare.com/client/v4/ips and can be formated using | python3 -m json.tool
   109  // Changelog: https://www.cloudflare.com/en-gb/ips/
   110  // API Docs: https://developers.cloudflare.com/api/operations/cloudflare-i-ps-cloudflare-ip-details
   111  func (client *Client) FetchCloudflareIps(ctx context.Context) (res CloudflareIpsResult, err error) {
   112  	err = client.request(ctx, requestParams{
   113  		Method: http.MethodGet,
   114  		URL:    "/client/v4/ips",
   115  	}, &res)
   116  	if err != nil {
   117  		return
   118  	}
   119  
   120  	return
   121  }