Repository: bacalhau-project/bacalhau (language: python)

Issue:
environment output should automatically update with external IP (if available) instead of 0.0.0.0



Discussion / hints:
For choosing the IP to listen on for a requester node (non-devstack) we should

* List IPs via net.InterfaceAddrs()
* Categorise into RFC1918/loopback/public
* Pick from Public, Private, Loopback (in that order)
* Provide explicit binding option for edge cases (allow multiples)

Edge cases:

* Multiple external addresses

Fix patch:
diff --git a/pkg/config/getters.go b/pkg/config/getters.go
index 842ca1f4d8..6cebef7749 100644
--- a/pkg/config/getters.go
+++ b/pkg/config/getters.go
@@ -2,16 +2,19 @@ package config
 
 import (
 	"fmt"
+	"net"
 	"os"
 	"strings"
 	"time"
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/multiformats/go-multiaddr"
+	"github.com/pkg/errors"
 	"github.com/rs/zerolog/log"
 	"github.com/spf13/viper"
 
 	"github.com/bacalhau-project/bacalhau/pkg/config/types"
+	"github.com/bacalhau-project/bacalhau/pkg/lib/network"
 	"github.com/bacalhau-project/bacalhau/pkg/logger"
 )
 
@@ -53,8 +56,38 @@ func ServerAPIPort() uint16 {
 	return uint16(viper.GetInt(types.NodeServerAPIPort))
 }
 
+func configError(e error) {
+	msg := fmt.Sprintf("config error: %s", e)
+	fmt.Fprintln(os.Stderr, msg)
+	os.Exit(1)
+}
+
 func ServerAPIHost() string {
-	return viper.GetString(types.NodeServerAPIHost)
+	host := viper.GetString(types.NodeServerAPIHost)
+
+	if net.ParseIP(host) == nil {
+		// We should check that the value gives us an address type
+		// we can use to get our IP address. If it doesn't, we should
+		// panic.
+		atype, ok := network.AddressTypeFromString(host)
+		if !ok {
+			configError(fmt.Errorf("invalid address type in Server API Host config: %s", host))
+		}
+
+		addr, err := network.GetNetworkAddress(atype, network.AllAddresses)
+		if err != nil {
+			configError(errors.Wrap(err, fmt.Sprintf("failed to get network address for Server API Host: %s", host)))
+		}
+
+		if len(addr) == 0 {
+			configError(fmt.Errorf("no %s addresses found for Server API Host", host))
+		}
+
+		// Use the first address
+		host = addr[0]
+	}
+
+	return host
 }
 
 func ServerAutoCertDomain() string {
diff --git a/pkg/lib/network/network.go b/pkg/lib/network/network.go
new file mode 100644
index 0000000000..8a166aa382
--- /dev/null
+++ b/pkg/lib/network/network.go
@@ -0,0 +1,119 @@
+package network
+
+import (
+	"fmt"
+	"net"
+	"strings"
+
+	"github.com/pkg/errors"
+)
+
+type AddressType int
+
+const (
+	PrivateAddress AddressType = iota
+	PublicAddress
+	LoopbackAddress
+	LinkLocal
+	Multicast
+	Any
+)
+
+type AddressLister func() ([]net.IP, error)
+
+func AllAddresses() ([]net.IP, error) {
+	var result []net.IP
+
+	addresses, err := net.InterfaceAddrs()
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to get interface addresses")
+	}
+
+	for _, address := range addresses {
+		ip, _, err := net.ParseCIDR(address.String())
+		if err != nil {
+			return nil, errors.Wrap(err, fmt.Sprintf("failed to parse address: %s", address.String()))
+		}
+
+		ipAs4 := ip.To4()
+		if ipAs4 != nil {
+			result = append(result, ipAs4)
+		}
+	}
+
+	return result, nil
+}
+
+// GetNetworkAddress returns a list of network addresses of the requested type,
+// sourcing the addresses from the provided AddressLister. It is expected that
+// network.AddAddresses() will be the default address lister. The result is
+// a list of strings representing the network addresses in the order they
+// were returned by the AddressLister.
+func GetNetworkAddress(requested AddressType, getAddresses AddressLister) ([]string, error) {
+	addresses, err := getAddresses()
+	if err != nil {
+		return nil, err
+	}
+
+	result := make([]string, 0, len(addresses))
+
+	for i := range addresses {
+		addr := addresses[i]
+
+		switch requested {
+		case Any:
+			result = append(result, addr.String())
+		case PrivateAddress:
+			if addr.IsPrivate() || isCarrierGradeNAT(addr) {
+				result = append(result, addr.String())
+			}
+		case PublicAddress:
+			if !addr.IsPrivate() && addr.IsGlobalUnicast() && !isCarrierGradeNAT(addr) {
+				result = append(result, addr.String())
+			}
+		case LoopbackAddress:
+			if addr.IsLoopback() {
+				result = append(result, addr.String())
+			}
+		case LinkLocal:
+			if addr.IsLinkLocalMulticast() || addr.IsLinkLocalUnicast() {
+				result = append(result, addr.String())
+			}
+		case Multicast:
+			if isMulticastAddress(addr) {
+				result = append(result, addr.String())
+			}
+		}
+	}
+
+	return result, nil
+}
+
+func AddressTypeFromString(t string) (AddressType, bool) {
+	switch strings.ToLower(t) {
+	case "private":
+		return PrivateAddress, true
+	case "public":
+		return PublicAddress, true
+	case "loopback", "localhost", "local":
+		return LoopbackAddress, true
+	case "linklocal":
+		return LinkLocal, true
+	case "multicast":
+		return Multicast, true
+	case "any":
+		return Any, true
+	default:
+		return Any, false
+	}
+}
+
+func isCarrierGradeNAT(addr net.IP) bool {
+	_, net, _ := net.ParseCIDR("100.64.0.0/10")
+	return net.Contains(addr)
+}
+
+func isMulticastAddress(addr net.IP) bool {
+	_, net, _ := net.ParseCIDR("224.0.0.0/4")
+	return net.Contains(addr)
+}


Test patch:
diff --git a/pkg/lib/network/network_test.go b/pkg/lib/network/network_test.go
new file mode 100644
index 0000000000..7ad9519a02
--- /dev/null
+++ b/pkg/lib/network/network_test.go
@@ -0,0 +1,67 @@
+//go:build unit || !integration
+
+package network_test
+
+import (
+	"net"
+	"testing"
+
+	"github.com/bacalhau-project/bacalhau/pkg/lib/network"
+	"github.com/stretchr/testify/suite"
+)
+
+type NetworkTestSuite struct {
+	suite.Suite
+}
+
+func TestNetworkSuite(t *testing.T) {
+	suite.Run(t, new(NetworkTestSuite))
+}
+
+func test_addresses() []string {
+	return []string{
+		"127.0.0.1",       // localhost
+		"169.254.46.5",    // link local
+		"192.168.0.1",     // private
+		"10.10.10.10",     // private
+		"172.16.20.24",    // private
+		"225.255.255.255", // multicast
+		"86.13.65.80",     //public
+	}
+}
+
+func AddressList() ([]net.IP, error) {
+	addr := test_addresses()
+	result := make([]net.IP, len(addr))
+
+	for i := range addr {
+		result[i] = net.ParseIP(addr[i])
+	}
+
+	return result, nil
+}
+
+func (s *NetworkTestSuite) TestFetch() {
+	type testcase struct {
+		name        string
+		addressType network.AddressType
+		expected    []string
+	}
+	testcases := []testcase{
+		{"localhost", network.LoopbackAddress, []string{"127.0.0.1"}},
+		{"linklocal", network.LinkLocal, []string{"169.254.46.5"}},
+		{"multicast", network.Multicast, []string{"225.255.255.255"}},
+		{"private", network.PrivateAddress, []string{"192.168.0.1", "10.10.10.10", "172.16.20.24"}},
+		{"public", network.PublicAddress, []string{"86.13.65.80"}},
+		{"any", network.Any, test_addresses()},
+	}
+
+	for t := range testcases {
+		tc := testcases[t]
+		s.Run(tc.name, func() {
+			addr, err := network.GetNetworkAddress(tc.addressType, AddressList)
+			s.Require().NoError(err)
+			s.Require().ElementsMatch(tc.expected, addr)
+		})
+	}
+}