From 3805036d7ea2ce1103dad37e7ba3e9b62e05858b Mon Sep 17 00:00:00 2001 From: Jack Wampler Date: Sat, 7 Oct 2023 12:10:15 -0600 Subject: [PATCH] Phantom selection centralization (#241) centralizing and fleshing out tests for phantom selections including backward compatibility --- internal/compatability/README.md | 23 + internal/compatability/v0/compat.go | 277 ++++++++ internal/compatability/v1/compat.go | 257 ++++++++ pkg/phantoms/compat.go | 244 +++++++ pkg/phantoms/compat_test.go | 204 ++++++ pkg/phantoms/phantom_selector.go | 189 ++++++ .../lib => phantoms}/phantom_selector_test.go | 292 ++++----- pkg/phantoms/phantoms.go | 172 +---- pkg/phantoms/phantoms_test.go | 6 +- pkg/phantoms/station_phantoms.go | 193 ++++++ pkg/phantoms/test/phantom_subnets.toml | 25 + pkg/phantoms/test/phantom_subnets_update.toml | 32 + pkg/regserver/apiregserver/apiregserver.go | 4 +- pkg/regserver/regprocessor/regprocessor.go | 15 +- .../regprocessor/regprocessor_test.go | 12 +- pkg/station/lib/phantom_selector.go | 616 ------------------ pkg/station/lib/phantoms.go | 69 -- pkg/station/lib/phantoms_test.go | 25 - pkg/station/lib/registration.go | 7 +- pkg/station/lib/registration_ingest.go | 9 +- 20 files changed, 1612 insertions(+), 1059 deletions(-) create mode 100644 internal/compatability/README.md create mode 100644 internal/compatability/v0/compat.go create mode 100644 internal/compatability/v1/compat.go create mode 100644 pkg/phantoms/compat.go create mode 100644 pkg/phantoms/compat_test.go create mode 100644 pkg/phantoms/phantom_selector.go rename pkg/{station/lib => phantoms}/phantom_selector_test.go (50%) create mode 100644 pkg/phantoms/station_phantoms.go create mode 100644 pkg/phantoms/test/phantom_subnets.toml create mode 100644 pkg/phantoms/test/phantom_subnets_update.toml delete mode 100644 pkg/station/lib/phantom_selector.go delete mode 100644 pkg/station/lib/phantoms.go delete mode 100644 pkg/station/lib/phantoms_test.go diff --git a/internal/compatability/README.md b/internal/compatability/README.md new file mode 100644 index 00000000..a0d5c848 --- /dev/null +++ b/internal/compatability/README.md @@ -0,0 +1,23 @@ + +# Compatibility Packages + +The station attempts to support older versions of the client. In order to ensure that the features +that have changed over the evolution of the client library this package and it's sub-packages +contain the original client side implementations for the station to test against. + +These packages should NOT be used for real clients, or even included in non-test portions of the +regular Conjure library. + +```tree +compatability +├── README.md +├── v0 +│ └── compat.go +└── v1 + └── compat.go +``` + +**V0** compatibility requires support for the original buggy phantom selection algorithm + +**V1** compatibility requires support for the updated, but still `math/rand` based varint phantom +selection algorithm diff --git a/internal/compatability/v0/compat.go b/internal/compatability/v0/compat.go new file mode 100644 index 00000000..dd87b2b7 --- /dev/null +++ b/internal/compatability/v0/compat.go @@ -0,0 +1,277 @@ +package v0 + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math/big" + "math/rand" + "net" + + wr "github.com/mroth/weightedrand" + pb "github.com/refraction-networking/conjure/proto" +) + +var ErrorV0SelectionBug = errors.New("let's rewrite the phantom address selector") +var ErrSubnetParseBug = errors.New("no subnets provided") + +// getSubnets - return EITHER all subnet strings as one composite array if we are +// +// selecting unweighted, or return the array associated with the (seed) selected +// array of subnet strings based on the associated weights +func getSubnets(sc *pb.PhantomSubnetsList, seed []byte, weighted bool) []string { + + var out []string = []string{} + + if weighted { + // seed random with hkdf derived seed provided by client + seedInt, err := binary.ReadVarint(bytes.NewBuffer(seed)) + if err != nil { + return nil + } + rand.Seed(seedInt) + + weightedSubnets := sc.GetWeightedSubnets() + if weightedSubnets == nil { + return []string{} + } + + choices := make([]wr.Choice, 0, len(weightedSubnets)) + + // fmt.Println("DEBUG - len = ", len(weightedSubnets)) + for _, cjSubnet := range weightedSubnets { + weight := cjSubnet.GetWeight() + subnets := cjSubnet.GetSubnets() + if subnets == nil { + continue + } + // fmt.Println("Adding Choice", subnets, weight) + choices = append(choices, wr.Choice{Item: subnets, Weight: uint(weight)}) + } + + c, _ := wr.NewChooser(choices...) + if c == nil { + return []string{} + } + + out = c.Pick().([]string) + } else { + + weightedSubnets := sc.GetWeightedSubnets() + if weightedSubnets == nil { + return []string{} + } + + // Use unweighted config for subnets, concat all into one array and return. + for _, cjSubnet := range weightedSubnets { + out = append(out, cjSubnet.Subnets...) + } + } + + return out +} + +// SubnetFilter - Filter IP subnets based on whatever to prevent specific subnets from +// +// inclusion in choice. See v4Only and v6Only for reference. +type SubnetFilter func([]*net.IPNet) ([]*net.IPNet, error) + +func V4Only(obj []*net.IPNet) ([]*net.IPNet, error) { + var out []*net.IPNet = []*net.IPNet{} + + for _, _net := range obj { + if ipv4net := _net.IP.To4(); ipv4net != nil { + out = append(out, _net) + } + } + return out, nil +} + +// V6Only - a functor for transforming the subnet list to only include IPv6 subnets +func V6Only(obj []*net.IPNet) ([]*net.IPNet, error) { + var out []*net.IPNet = []*net.IPNet{} + + for _, _net := range obj { + if _net.IP == nil { + continue + } + if net := _net.IP.To4(); net != nil { + continue + } + out = append(out, _net) + } + return out, nil +} + +func parseSubnets(phantomSubnets []string) ([]*net.IPNet, error) { + var subnets []*net.IPNet = []*net.IPNet{} + + if len(phantomSubnets) == 0 { + return nil, fmt.Errorf("parseSubnets - %w", ErrSubnetParseBug) + } + + for _, strNet := range phantomSubnets { + _, parsedNet, err := net.ParseCIDR(strNet) + if err != nil { + return nil, err + } + if parsedNet == nil { + return nil, fmt.Errorf("failed to parse %v as subnet", parsedNet) + } + + subnets = append(subnets, parsedNet) + } + + return subnets, nil + // return nil, fmt.Errorf("parseSubnets not implemented yet") +} + +// SelectAddrFromSubnet - given a seed and a CIDR block choose an address. +// +// This is done by generating a seeded random bytes up to teh length of the +// full address then using the net mask to zero out any bytes that are +// already specified by the CIDR block. Tde masked random value is then +// added to the cidr block base giving the final randomly selected address. +func SelectAddrFromSubnet(seed []byte, net1 *net.IPNet) (net.IP, error) { + bits, addrLen := net1.Mask.Size() + + ipBigInt := &big.Int{} + if v4net := net1.IP.To4(); v4net != nil { + ipBigInt.SetBytes(net1.IP.To4()) + } else if v6net := net1.IP.To16(); v6net != nil { + ipBigInt.SetBytes(net1.IP.To16()) + } + + seedInt, err := binary.ReadVarint(bytes.NewBuffer(seed)) + if err != nil { + return nil, err + } + + rand.Seed(seedInt) + randBytes := make([]byte, addrLen/8) + _, err = rand.Read(randBytes) + if err != nil { + return nil, err + } + randBigInt := &big.Int{} + randBigInt.SetBytes(randBytes) + + mask := make([]byte, addrLen/8) + for i := 0; i < addrLen/8; i++ { + mask[i] = 0xff + } + maskBigInt := &big.Int{} + maskBigInt.SetBytes(mask) + maskBigInt.Rsh(maskBigInt, uint(bits)) + + randBigInt.And(randBigInt, maskBigInt) + ipBigInt.Add(ipBigInt, randBigInt) + + return net.IP(ipBigInt.Bytes()), nil +} + +func selectIPAddr(seed []byte, subnets []*net.IPNet) (*net.IP, error) { + + addresses_total := big.NewInt(0) + + type idNet struct { + min, max big.Int + net *net.IPNet + } + var idNets []idNet + + for _, _net := range subnets { + netMaskOnes, _ := _net.Mask.Size() + if ipv4net := _net.IP.To4(); ipv4net != nil { + _idNet := idNet{} + _idNet.min.Set(addresses_total) + addresses_total.Add(addresses_total, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(32-netMaskOnes)), nil)) + addresses_total.Sub(addresses_total, big.NewInt(1)) + _idNet.max.Set(addresses_total) + _idNet.net = _net + idNets = append(idNets, _idNet) + } else if ipv6net := _net.IP.To16(); ipv6net != nil { + _idNet := idNet{} + _idNet.min.Set(addresses_total) + addresses_total.Add(addresses_total, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(128-netMaskOnes)), nil)) + addresses_total.Sub(addresses_total, big.NewInt(1)) + _idNet.max.Set(addresses_total) + _idNet.net = _net + idNets = append(idNets, _idNet) + } else { + return nil, fmt.Errorf("failed to parse %v", _net) + } + } + + if addresses_total.Cmp(big.NewInt(0)) <= 0 { + return nil, fmt.Errorf("No valid addresses specified") + } + + id := &big.Int{} + id.SetBytes(seed) + if id.Cmp(addresses_total) > 0 { + id.Mod(id, addresses_total) + } + + var result net.IP + var err error + for _, _idNet := range idNets { + if _idNet.max.Cmp(id) >= 0 && _idNet.min.Cmp(id) == -1 { + result, err = SelectAddrFromSubnet(seed, _idNet.net) + if err != nil { + return nil, fmt.Errorf("Failed to chose IP address: %v", err) + } + } + } + if result == nil { + return nil, ErrorV0SelectionBug + } + return &result, nil +} + +// SelectPhantom - select one phantom IP address based on shared secret +func SelectPhantom(seed []byte, subnetsList *pb.PhantomSubnetsList, transform SubnetFilter, weighted bool) (*net.IP, error) { + + s, err := parseSubnets(getSubnets(subnetsList, seed, weighted)) + if err != nil { + return nil, fmt.Errorf("Failed to parse subnets: %w", err) + } + + if transform != nil { + s, err = transform(s) + if err != nil { + return nil, err + } + } + + return selectIPAddr(seed, s) +} + +// SelectPhantomUnweighted - select one phantom IP address based on shared secret +func SelectPhantomUnweighted(seed []byte, subnets *pb.PhantomSubnetsList, transform SubnetFilter) (*net.IP, error) { + return SelectPhantom(seed, subnets, transform, false) +} + +// SelectPhantomWeighted - select one phantom IP address based on shared secret +func SelectPhantomWeighted(seed []byte, subnets *pb.PhantomSubnetsList, transform SubnetFilter) (*net.IP, error) { + return SelectPhantom(seed, subnets, transform, true) +} + +// GetDefaultPhantomSubnets implements the +func GetDefaultPhantomSubnets() *pb.PhantomSubnetsList { + var w1 = uint32(9.0) + var w2 = uint32(1.0) + return &pb.PhantomSubnetsList{ + WeightedSubnets: []*pb.PhantomSubnets{ + { + Weight: &w1, + Subnets: []string{"192.122.190.0/24", "2001:48a8:687f:1::/64"}, + }, + { + Weight: &w2, + Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}, + }, + }, + } +} diff --git a/internal/compatability/v1/compat.go b/internal/compatability/v1/compat.go new file mode 100644 index 00000000..447f8b5b --- /dev/null +++ b/internal/compatability/v1/compat.go @@ -0,0 +1,257 @@ +package v1 + +import ( + "encoding/binary" + "errors" + "fmt" + "math/big" + "math/rand" + "net" + + wr "github.com/mroth/weightedrand" + pb "github.com/refraction-networking/conjure/proto" +) + +// getSubnets - return EITHER all subnet strings as one composite array if we are +// +// selecting unweighted, or return the array associated with the (seed) selected +// array of subnet strings based on the associated weights +func getSubnets(sc *pb.PhantomSubnetsList, seed []byte, weighted bool) []string { + + var out []string = []string{} + + if weighted { + // seed random with hkdf derived seed provided by client + seedInt, n := binary.Varint(seed) + if n == 0 { + // fmt.Println("failed to seed random for weighted rand") + return nil + } + rand.Seed(seedInt) + + weightedSubnets := sc.GetWeightedSubnets() + if weightedSubnets == nil { + return []string{} + } + + choices := make([]wr.Choice, 0, len(weightedSubnets)) + + // fmt.Println("DEBUG - len = ", len(weightedSubnets)) + for _, cjSubnet := range weightedSubnets { + weight := cjSubnet.GetWeight() + subnets := cjSubnet.GetSubnets() + if subnets == nil { + continue + } + // fmt.Println("Adding Choice", subnets, weight) + choices = append(choices, wr.Choice{Item: subnets, Weight: uint(weight)}) + } + + c, _ := wr.NewChooser(choices...) + if c == nil { + return []string{} + } + + out = c.Pick().([]string) + } else { + + weightedSubnets := sc.GetWeightedSubnets() + if weightedSubnets == nil { + return []string{} + } + + // Use unweighted config for subnets, concat all into one array and return. + for _, cjSubnet := range weightedSubnets { + out = append(out, cjSubnet.Subnets...) + } + } + + return out +} + +// SubnetFilter - Filter IP subnets based on whatever to prevent specific subnets from +// +// inclusion in choice. See v4Only and v6Only for reference. +type SubnetFilter func([]*net.IPNet) ([]*net.IPNet, error) + +func V4Only(obj []*net.IPNet) ([]*net.IPNet, error) { + var out []*net.IPNet = []*net.IPNet{} + + for _, _net := range obj { + if ipv4net := _net.IP.To4(); ipv4net != nil { + out = append(out, _net) + } + } + return out, nil +} + +// V6Only - a functor for transforming the subnet list to only include IPv6 subnets +func V6Only(obj []*net.IPNet) ([]*net.IPNet, error) { + var out []*net.IPNet = []*net.IPNet{} + + for _, _net := range obj { + if _net.IP == nil { + continue + } + if net := _net.IP.To4(); net != nil { + continue + } + out = append(out, _net) + } + return out, nil +} + +func parseSubnets(phantomSubnets []string) ([]*net.IPNet, error) { + var subnets []*net.IPNet = []*net.IPNet{} + + if len(phantomSubnets) == 0 { + return nil, fmt.Errorf("parseSubnets - no subnets provided") + } + + for _, strNet := range phantomSubnets { + _, parsedNet, err := net.ParseCIDR(strNet) + if err != nil { + return nil, err + } + if parsedNet == nil { + return nil, fmt.Errorf("failed to parse %v as subnet", parsedNet) + } + + subnets = append(subnets, parsedNet) + } + + return subnets, nil + // return nil, fmt.Errorf("parseSubnets not implemented yet") +} + +// SelectAddrFromSubnet given a seed and a CIDR block choose an address. This is +// done by generating a seeded random bytes up to teh length of the full address +// then using the net mask to zero out any bytes that are already specified by +// the CIDR block. Tde masked random value is then added to the cidr block base +// giving the final randomly selected address. +func SelectAddrFromSubnet(seed []byte, net1 *net.IPNet) (net.IP, error) { + bits, addrLen := net1.Mask.Size() + + ipBigInt := &big.Int{} + if v4net := net1.IP.To4(); v4net != nil { + ipBigInt.SetBytes(net1.IP.To4()) + } else if v6net := net1.IP.To16(); v6net != nil { + ipBigInt.SetBytes(net1.IP.To16()) + } + + seedInt, n := binary.Varint(seed) + if n == 0 { + return nil, fmt.Errorf("failed to create seed ") + } + + rand.Seed(seedInt) + randBytes := make([]byte, addrLen/8) + _, err := rand.Read(randBytes) + if err != nil { + return nil, err + } + randBigInt := &big.Int{} + randBigInt.SetBytes(randBytes) + + mask := make([]byte, addrLen/8) + for i := 0; i < addrLen/8; i++ { + mask[i] = 0xff + } + maskBigInt := &big.Int{} + maskBigInt.SetBytes(mask) + maskBigInt.Rsh(maskBigInt, uint(bits)) + + randBigInt.And(randBigInt, maskBigInt) + ipBigInt.Add(ipBigInt, randBigInt) + + return net.IP(ipBigInt.Bytes()), nil +} + +// selectIPAddr selects an ip address from the list of subnets associated +// with the specified generation by constructing a set of start and end values +// for the high and low values in each allocation. The random number is then +// bound between the global min and max of that set. This ensures that +// addresses are chosen based on the number of addresses in the subnet. +func selectIPAddr(seed []byte, subnets []*net.IPNet) (*net.IP, error) { + type idNet struct { + min, max big.Int + net net.IPNet + } + var idNets []idNet + + // Compose a list of ID Nets with min, max and network associated and count + // the total number of available addresses. + addressTotal := big.NewInt(0) + for _, _net := range subnets { + netMaskOnes, _ := _net.Mask.Size() + if ipv4net := _net.IP.To4(); ipv4net != nil { + _idNet := idNet{} + _idNet.min.Set(addressTotal) + addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(32-netMaskOnes)), nil)) + _idNet.max.Sub(addressTotal, big.NewInt(1)) + _idNet.net = *_net + idNets = append(idNets, _idNet) + } else if ipv6net := _net.IP.To16(); ipv6net != nil { + _idNet := idNet{} + _idNet.min.Set(addressTotal) + addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(128-netMaskOnes)), nil)) + _idNet.max.Sub(addressTotal, big.NewInt(1)) + _idNet.net = *_net + idNets = append(idNets, _idNet) + } else { + return nil, fmt.Errorf("failed to parse %v", _net) + } + } + + // If the total number of addresses is 0 something has gone wrong + if addressTotal.Cmp(big.NewInt(0)) <= 0 { + return nil, fmt.Errorf("no valid addresses specified") + } + + // Pick a value using the seed in the range of between 0 and the total + // number of addresses. + id := &big.Int{} + id.SetBytes(seed) + if id.Cmp(addressTotal) >= 0 { + id.Mod(id, addressTotal) + } + + // Find the network (ID net) that contains our random value and select a + // random address from that subnet. + // min >= id%total >= max + var result net.IP + var err error + for _, _idNet := range idNets { + // fmt.Printf("tot:%s, seed%%tot:%s id cmp max: %d, id cmp min: %d %s\n", addressTotal.String(), id, _idNet.max.Cmp(id), _idNet.min.Cmp(id), _idNet.net.String()) + if _idNet.max.Cmp(id) >= 0 && _idNet.min.Cmp(id) <= 0 { + result, err = SelectAddrFromSubnet(seed, &_idNet.net) + if err != nil { + return nil, fmt.Errorf("failed to chose IP address: %v", err) + } + } + } + + // We want to make it so this CANNOT happen + if result == nil { + return nil, errors.New("nil result should not be possible") + } + return &result, nil +} + +// SelectPhantom - select one phantom IP address based on shared secret +func SelectPhantom(seed []byte, subnetsList *pb.PhantomSubnetsList, transform SubnetFilter, weighted bool) (*net.IP, error) { + + s, err := parseSubnets(getSubnets(subnetsList, seed, weighted)) + if err != nil { + return nil, fmt.Errorf("failed to parse subnets: %v", err) + } + + if transform != nil { + s, err = transform(s) + if err != nil { + return nil, err + } + } + + return selectIPAddr(seed, s) +} diff --git a/pkg/phantoms/compat.go b/pkg/phantoms/compat.go new file mode 100644 index 00000000..32a6d6a8 --- /dev/null +++ b/pkg/phantoms/compat.go @@ -0,0 +1,244 @@ +package phantoms + +import ( + "encoding/binary" + "errors" + "fmt" + "math/big" + mrand "math/rand" + "net" + "time" + + wr "github.com/mroth/weightedrand" + pb "github.com/refraction-networking/conjure/proto" +) + +// getSubnetsVarint - return EITHER all subnet strings as one composite array if +// we are selecting unweighted, or return the array associated with the (seed) +// selected array of subnet strings based on the associated weights +// +// Used by Client version 0 and 1 +func (sc *SubnetConfig) getSubnetsVarint(seed []byte, weighted bool) ([]*phantomNet, error) { + + if weighted { + // seed random with hkdf derived seed provided by client + seedInt, n := binary.Varint(seed) + if n == 0 { + return nil, fmt.Errorf("failed to seed random for weighted rand") + } + + // nolint:staticcheck // here for backwards compatibility with clients + mrand.Seed(seedInt) + + choices := make([]wr.Choice, 0, len(sc.WeightedSubnets)) + for _, cjSubnet := range sc.WeightedSubnets { + cjSubnet := cjSubnet // copy loop ptr + choices = append(choices, wr.Choice{Item: cjSubnet, Weight: uint(cjSubnet.GetWeight())}) + } + c, err := wr.NewChooser(choices...) + if err != nil { + return nil, err + } + + return parseSubnets(c.Pick().(*pb.PhantomSubnets)) + + } + + // Use unweighted config for subnets, concat all into one array and return. + out := []*phantomNet{} + for _, cjSubnet := range sc.WeightedSubnets { + nets, err := parseSubnets(cjSubnet) + if err != nil { + return nil, fmt.Errorf("error parsing subnet: %v", err) + } + out = append(out, nets...) + } + + return out, nil +} + +// selectPhantomImplVarint - select an ip address from the list of subnets +// associated with the specified generation by constructing a set of start and +// end values for the high and low values in each allocation. The random number +// is then bound between the global min and max of that set. This ensures that +// addresses are chosen based on the number of addresses in the subnet. +func selectPhantomImplVarint(seed []byte, subnets []*phantomNet) (*PhantomIP, error) { + type idNet struct { + min, max big.Int + net *phantomNet + } + var idNets []idNet + + // Compose a list of ID Nets with min, max and network associated and count + // the total number of available addresses. + addressTotal := big.NewInt(0) + for _, _net := range subnets { + netMaskOnes, _ := _net.Mask.Size() + if ipv4net := _net.IP.To4(); ipv4net != nil { + _idNet := idNet{} + _idNet.min.Set(addressTotal) + addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(32-netMaskOnes)), nil)) + _idNet.max.Sub(addressTotal, big.NewInt(1)) + _idNet.net = _net + idNets = append(idNets, _idNet) + } else if ipv6net := _net.IP.To16(); ipv6net != nil { + _idNet := idNet{} + _idNet.min.Set(addressTotal) + addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(128-netMaskOnes)), nil)) + _idNet.max.Sub(addressTotal, big.NewInt(1)) + _idNet.net = _net + idNets = append(idNets, _idNet) + } else { + return nil, fmt.Errorf("failed to parse %v", _net) + } + } + + // If the total number of addresses is 0 something has gone wrong + if addressTotal.Cmp(big.NewInt(0)) <= 0 { + return nil, ErrLegacyAddrSelectBug + } + + // Pick a value using the seed in the range of between 0 and the total + // number of addresses. + id := &big.Int{} + id.SetBytes(seed) + if id.Cmp(addressTotal) >= 0 { + id.Mod(id, addressTotal) + } + + // Find the network (ID net) that contains our random value and select a + // random address from that subnet. + // min >= id%total >= max + var result *PhantomIP + for _, _idNet := range idNets { + // fmt.Printf("tot:%s, seed%%tot:%s id cmp max: %d, id cmp min: %d %s\n", addressTotal.String(), id, _idNet.max.Cmp(id), _idNet.min.Cmp(id), _idNet.net.String()) + if _idNet.max.Cmp(id) >= 0 && _idNet.min.Cmp(id) <= 0 { + res, err := SelectAddrFromSubnet(seed, _idNet.net.IPNet) + if err != nil { + return nil, fmt.Errorf("failed to chose IP address: %v", err) + } + + result = &PhantomIP{ip: &res, supportRandomPort: _idNet.net.supportRandomPort} + } + } + + // We want to make it so this CANNOT happen + if result == nil { + return nil, errors.New("nil result should not be possible") + } + return result, nil +} + +// selectPhantomImplV0 implements support for the legacy (buggy) client phantom +// address selection algorithm. +func selectPhantomImplV0(seed []byte, subnets []*phantomNet) (*PhantomIP, error) { + + addressTotal := big.NewInt(0) + + type idNet struct { + min, max big.Int + net *phantomNet + } + var idNets []idNet + + for _, _net := range subnets { + netMaskOnes, _ := _net.Mask.Size() + if ipv4net := _net.IP.To4(); ipv4net != nil { + _idNet := idNet{} + _idNet.min.Set(addressTotal) + addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(32-netMaskOnes)), nil)) + addressTotal.Sub(addressTotal, big.NewInt(1)) + _idNet.max.Set(addressTotal) + _idNet.net = _net + idNets = append(idNets, _idNet) + } else if ipv6net := _net.IP.To16(); ipv6net != nil { + _idNet := idNet{} + _idNet.min.Set(addressTotal) + addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(128-netMaskOnes)), nil)) + addressTotal.Sub(addressTotal, big.NewInt(1)) + _idNet.max.Set(addressTotal) + _idNet.net = _net + idNets = append(idNets, _idNet) + } else { + return nil, fmt.Errorf("failed to parse %v", _net) + } + } + + if addressTotal.Cmp(big.NewInt(0)) <= 0 { + return nil, ErrLegacyMissingAddrs + } + + id := &big.Int{} + id.SetBytes(seed) + if id.Cmp(addressTotal) > 0 { + id.Mod(id, addressTotal) + } + + var result *PhantomIP + for _, _idNet := range idNets { + if _idNet.max.Cmp(id) >= 0 && _idNet.min.Cmp(id) == -1 { + res, err := SelectAddrFromSubnet(seed, _idNet.net.IPNet) + if err != nil { + return nil, fmt.Errorf("failed to chose IP address: %v", err) + } + result = &PhantomIP{ip: &res, supportRandomPort: _idNet.net.supportRandomPort} + } + } + if result == nil { + return nil, ErrLegacyV0SelectionBug + } + return result, nil +} + +// SelectAddrFromSubnet - given a seed and a CIDR block choose an address. +// +// This is done by generating a seeded random bytes up to the length of the full +// address then using the net mask to zero out any bytes that are already +// specified by the CIDR block. Tde masked random value is then added to the +// cidr block base giving the final randomly selected address. +func SelectAddrFromSubnet(seed []byte, net1 *net.IPNet) (net.IP, error) { + bits, addrLen := net1.Mask.Size() + + ipBigInt := &big.Int{} + if v4net := net1.IP.To4(); v4net != nil { + ipBigInt.SetBytes(net1.IP.To4()) + } else if v6net := net1.IP.To16(); v6net != nil { + ipBigInt.SetBytes(net1.IP.To16()) + } + + seedInt, n := binary.Varint(seed) + if n == 0 { + return nil, fmt.Errorf("failed to create seed ") + } + + // nolint:staticcheck // here for backwards compatibility with clients + mrand.Seed(seedInt) + randBytes := make([]byte, addrLen/8) + + // nolint:staticcheck // here for backwards compatibility with clients + _, err := mrand.Read(randBytes) + if err != nil { + return nil, err + } + randBigInt := &big.Int{} + randBigInt.SetBytes(randBytes) + + mask := make([]byte, addrLen/8) + for i := 0; i < addrLen/8; i++ { + mask[i] = 0xff + } + maskBigInt := &big.Int{} + maskBigInt.SetBytes(mask) + maskBigInt.Rsh(maskBigInt, uint(bits)) + + randBigInt.And(randBigInt, maskBigInt) + ipBigInt.Add(ipBigInt, randBigInt) + + return net.IP(ipBigInt.Bytes()), nil +} + +func init() { + // NOTE: math/rand is only used for backwards compatibility. + // nolint:staticcheck + mrand.Seed(time.Now().UnixNano()) +} diff --git a/pkg/phantoms/compat_test.go b/pkg/phantoms/compat_test.go new file mode 100644 index 00000000..28cd1edb --- /dev/null +++ b/pkg/phantoms/compat_test.go @@ -0,0 +1,204 @@ +package phantoms + +import ( + "crypto/rand" + "encoding/hex" + "errors" + "os" + "testing" + + v0 "github.com/refraction-networking/conjure/internal/compatability/v0" + v1 "github.com/refraction-networking/conjure/internal/compatability/v1" + "github.com/refraction-networking/conjure/pkg/core" + pb "github.com/refraction-networking/conjure/proto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// This tests Client V1 +func TestPhantomsSeededSelectionVarint(t *testing.T) { + os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") + phantomSelector, err := NewPhantomIPSelector() + require.Nil(t, err, "Failed to create the PhantomIPSelector Object") + + var newConf = &SubnetConfig{ + WeightedSubnets: []*pb.PhantomSubnets{ + {Weight: proto.Uint32(9), Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}}, + {Weight: proto.Uint32(1), Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}}, + }, + } + + newGen := phantomSelector.AddGeneration(-1, newConf) + + seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") + expectedAddr := "192.122.190.130" + + phantomAddr, err := phantomSelector.Select(seed, newGen, 1, false) + require.Nil(t, err) + assert.Equal(t, expectedAddr, phantomAddr.String()) +} + +// Client V1 +func TestPhantomsSeededSelectionV6Varint(t *testing.T) { + os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") + phantomSelector, err := NewPhantomIPSelector() + require.Nil(t, err, "Failed to create the PhantomIPSelector Object") + + var newConf = &SubnetConfig{ + WeightedSubnets: []*pb.PhantomSubnets{ + {Weight: proto.Uint32(9), Subnets: []string{"192.122.190.0/24", "2001:48a8:687f:1::/64"}, RandomizeDstPort: proto.Bool(true)}, + {Weight: proto.Uint32(1), Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}, RandomizeDstPort: proto.Bool(true)}, + }, + } + + newGen := phantomSelector.AddGeneration(-1, newConf) + + seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") + + phantomAddr, err := phantomSelector.Select(seed, newGen, 1, true) + require.Nil(t, err) + assert.True(t, phantomAddr.To4() == nil) + assert.True(t, phantomAddr.To16() != nil) +} + +func TestPhantomsCompatV1(t *testing.T) { + os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") + phantomSelector, err := NewPhantomIPSelector() + require.Nil(t, err, "Failed to create the PhantomIPSelector Object") + seed := make([]byte, 32) + + for _, testSet := range testSetOfWeightedSubnetLists { + var psl = &pb.PhantomSubnetsList{WeightedSubnets: testSet} + var newConf = &SubnetConfig{WeightedSubnets: testSet} + newGen := phantomSelector.AddGeneration(-1, newConf) + + for i := 0; i < 10_000; i++ { + _, err := rand.Read(seed) + require.Nil(t, err) + clientAddr, clientErr := v1.SelectPhantom(seed, psl, v1.V4Only, true) + stationAddr, stationErr := phantomSelector.Select(seed, newGen, core.PhantomSelectionMinGeneration, false) + if stationErr != nil { + require.Equal(t, stationErr, clientErr) + } else { + require.Nil(t, clientErr) + require.Nil(t, stationErr) + require.Equal(t, clientAddr.String(), stationAddr.String(), "client:%s, station:%s", clientAddr, stationAddr) + } + + // Check IPv6 Match + require.Nil(t, err) + clientAddr, clientErr = v1.SelectPhantom(seed, psl, v1.V6Only, true) + stationAddr, stationErr = phantomSelector.Select(seed, newGen, core.PhantomSelectionMinGeneration, true) + if stationErr != nil { + require.Equal(t, stationErr, clientErr) + } else { + require.Nil(t, clientErr) + require.Nil(t, stationErr) + require.Equal(t, clientAddr.String(), stationAddr.String(), "client:%s, station:%s", clientAddr, stationAddr) + } + } + } +} + +func TestPhantomsSeededSelectionV4(t *testing.T) { + os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") + phantomSelector, err := NewPhantomIPSelector() + require.Nil(t, err, "Failed to create the PhantomIPSelector Object") + + var newConf = &SubnetConfig{ + WeightedSubnets: []*pb.PhantomSubnets{ + {Weight: proto.Uint32(9), Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}, RandomizeDstPort: proto.Bool(true)}, + {Weight: proto.Uint32(1), Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}, RandomizeDstPort: proto.Bool(true)}, + }, + } + + newGen := phantomSelector.AddGeneration(-1, newConf) + + seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") + expectedAddr := "192.122.190.130" + + phantomAddr, err := phantomSelector.Select(seed, newGen, 0, false) + require.Nil(t, err) + assert.Equal(t, expectedAddr, phantomAddr.String()) + +} + +// This tests Client V0 +func TestPhantomsSeededSelectionLegacy(t *testing.T) { + os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") + phantomSelector, err := NewPhantomIPSelector() + require.Nil(t, err, "Failed to create the PhantomIPSelector Object") + + var newConf = &SubnetConfig{ + WeightedSubnets: []*pb.PhantomSubnets{ + {Weight: proto.Uint32(9), Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}}, + {Weight: proto.Uint32(1), Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}}, + }, + } + + newGen := phantomSelector.AddGeneration(-1, newConf) + + seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") + expectedAddr := "192.122.190.130" + + phantomAddr, err := phantomSelector.Select(seed, newGen, 0, false) + require.Nil(t, err) + assert.Equal(t, expectedAddr, phantomAddr.String()) + +} + +func TestPhantomsCompatV0(t *testing.T) { + os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") + phantomSelector, err := NewPhantomIPSelector() + require.Nil(t, err, "Failed to create the PhantomIPSelector Object") + + seed := make([]byte, 32) + for _, testSet := range testSetOfWeightedSubnetLists { + var psl = &pb.PhantomSubnetsList{WeightedSubnets: testSet} + var newConf = &SubnetConfig{WeightedSubnets: testSet} + newGen := phantomSelector.AddGeneration(-1, newConf) + + for i := 0; i < 10_000; i++ { + _, err := rand.Read(seed) + require.Nil(t, err) + clientAddr, clientErr := v0.SelectPhantom(seed, psl, v0.V4Only, true) + stationAddr, stationErr := phantomSelector.Select(seed, newGen, 0, false) + func() { + if errors.Is(clientErr, v0.ErrSubnetParseBug) { + return // it is possible the errors don't match properly whe the client hits this bug + } else if stationErr != nil && clientErr != nil { + require.Equal(t, clientErr.Error(), stationErr.Error()) + } else { + require.Nil(t, stationErr) + require.Nil(t, clientErr) + require.NotNil(t, clientAddr) + require.NotNil(t, stationAddr) + if stationAddr != nil && clientAddr != nil { + require.Equal(t, clientAddr.String(), stationAddr.String(), "client:%s, station:%s", clientAddr, stationAddr) + } + } + }() + + // Check IPv6 Match + require.Nil(t, err) + clientAddr, clientErr = v0.SelectPhantom(seed, psl, v0.V6Only, true) + stationAddr, stationErr = phantomSelector.Select(seed, newGen, 0, true) + func() { + if errors.Is(clientErr, v0.ErrSubnetParseBug) { + return // it is possible the errors don't match properly whe the client hits this bug + } else if stationErr != nil && clientErr != nil { + require.Equal(t, clientErr.Error(), stationErr.Error()) + } else { + require.Nil(t, stationErr) + require.Nil(t, clientErr) + require.NotNil(t, clientAddr) + require.NotNil(t, stationAddr) + if stationAddr != nil && clientAddr != nil { + require.Equal(t, clientAddr.String(), stationAddr.String(), "client:%s, station:%s", clientAddr, stationAddr) + } + } + }() + } + } +} diff --git a/pkg/phantoms/phantom_selector.go b/pkg/phantoms/phantom_selector.go new file mode 100644 index 00000000..066a1e78 --- /dev/null +++ b/pkg/phantoms/phantom_selector.go @@ -0,0 +1,189 @@ +package phantoms + +import ( + "crypto/rand" + "crypto/sha256" + "errors" + "fmt" + "math/big" + "net" + "sort" + + pb "github.com/refraction-networking/conjure/proto" + "golang.org/x/crypto/hkdf" +) + +var ( + // ErrLegacyAddrSelectBug indicates that we have hit a corner case in a legacy address selection + // algorithm that causes phantom address selection to fail. + ErrLegacyAddrSelectBug = errors.New("no valid addresses specified") + ErrLegacyMissingAddrs = errors.New("No valid addresses specified") + ErrLegacyV0SelectionBug = errors.New("let's rewrite the phantom address selector") + + // ErrMissingAddrs indicates that no subnets were provided with addresses to select from. This + // is only valid for phantomHkdfMinVersion and newer. + ErrMissingAddrs = errors.New("no valid addresses specified to select") +) + +// getSubnetsHkdf returns EITHER all subnet strings as one composite array if +// we are selecting unweighted, or return the array associated with the (seed) +// selected array of subnet strings based on the associated weights. Random +// values are seeded using an hkdf function to prevent biases introduced by +// math/rand and varint. +// +// Used by Client version 2+ +func getSubnetsHkdf(sc genericSubnetConfig, seed []byte, weighted bool) ([]*phantomNet, error) { + + weightedSubnets := sc.GetWeightedSubnets() + if weightedSubnets == nil { + return []*phantomNet{}, nil + } + + if weighted { + choices := make([]*pb.PhantomSubnets, 0, len(weightedSubnets)) + + totWeight := int64(0) + for _, cjSubnet := range weightedSubnets { + cjSubnet := cjSubnet // copy loop ptr + weight := cjSubnet.GetWeight() + subnets := cjSubnet.GetSubnets() + if subnets == nil { + continue + } + + totWeight += int64(weight) + choices = append(choices, cjSubnet) + } + + // Sort choices ascending + sort.Slice(choices, func(i, j int) bool { + return choices[i].GetWeight() < choices[j].GetWeight() + }) + + // Naive method: get random int, subtract from weights until you are < 0 + hkdfReader := hkdf.New(sha256.New, seed, nil, []byte("phantom-select-subnet")) + totWeightBig := big.NewInt(totWeight) + rndBig, err := rand.Int(hkdfReader, totWeightBig) + if err != nil { + return nil, err + } + + // Decrement rnd by each weight until it's < 0 + rnd := rndBig.Int64() + for _, choice := range choices { + rnd -= int64(choice.GetWeight()) + if rnd < 0 { + return parseSubnets(choice) + } + } + + } + + // Use unweighted config for subnets, concat all into one array and return. + out := []*phantomNet{} + for _, cjSubnet := range weightedSubnets { + nets, err := parseSubnets(cjSubnet) + if err != nil { + return nil, fmt.Errorf("error parsing subnet: %v", err) + } + out = append(out, nets...) + } + + return out, nil +} + +func selectPhantomImplHkdf(seed []byte, subnets []*phantomNet) (*PhantomIP, error) { + type idNet struct { + min, max big.Int + net *phantomNet + } + var idNets []idNet + + // Compose a list of ID Nets with min, max and network associated and count + // the total number of available addresses. + addressTotal := big.NewInt(0) + for _, _net := range subnets { + netMaskOnes, _ := _net.Mask.Size() + if ipv4net := _net.IP.To4(); ipv4net != nil { + _idNet := idNet{} + _idNet.min.Set(addressTotal) + addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(32-netMaskOnes)), nil)) + _idNet.max.Sub(addressTotal, big.NewInt(1)) + _idNet.net = _net + idNets = append(idNets, _idNet) + } else if ipv6net := _net.IP.To16(); ipv6net != nil { + _idNet := idNet{} + _idNet.min.Set(addressTotal) + addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(128-netMaskOnes)), nil)) + _idNet.max.Sub(addressTotal, big.NewInt(1)) + _idNet.net = _net + idNets = append(idNets, _idNet) + } else { + return nil, fmt.Errorf("failed to parse %v", _net) + } + } + + // If the total number of addresses is 0 something has gone wrong + if addressTotal.Cmp(big.NewInt(0)) <= 0 { + return nil, ErrMissingAddrs + } + + // Pick a value using the seed in the range of between 0 and the total + // number of addresses. + hkdfReader := hkdf.New(sha256.New, seed, nil, []byte("phantom-addr-id")) + id, err := rand.Int(hkdfReader, addressTotal) + if err != nil { + return nil, err + } + + // Find the network (ID net) that contains our random value and select a + // random address from that subnet. + // min >= id%total >= max + var result *PhantomIP + for _, _idNet := range idNets { + // fmt.Printf("tot:%s, seed%%tot:%s id cmp max: %d, id cmp min: %d %s\n", addressTotal.String(), id, _idNet.max.Cmp(id), _idNet.min.Cmp(id), _idNet.net.String()) + if _idNet.max.Cmp(id) >= 0 && _idNet.min.Cmp(id) <= 0 { + + var offset big.Int + offset.Sub(id, &_idNet.min) + result, err = selectAddrFromSubnetOffset(_idNet.net, &offset) + if err != nil { + return nil, fmt.Errorf("failed to chose IP address: %v", err) + } + } + } + + // We want to make it so this CANNOT happen + if result == nil { + return nil, errors.New("nil result should not be possible") + } + return result, nil +} + +// selectAddrFromSubnetOffset given a CIDR block and offset, return the net.IP +// +// Version 2: HKDF-based +func selectAddrFromSubnetOffset(net1 *phantomNet, offset *big.Int) (*PhantomIP, error) { + bits, addrLen := net1.Mask.Size() + + // Compute network size (e.g. an ipv4 /24 is 2^(32-24) + var netSize big.Int + netSize.Exp(big.NewInt(2), big.NewInt(int64(addrLen-bits)), nil) + + // Check that offset is within this subnet + if netSize.Cmp(offset) <= 0 { + return nil, errors.New("offset too big for subnet") + } + + ipBigInt := &big.Int{} + if v4net := net1.IP.To4(); v4net != nil { + ipBigInt.SetBytes(net1.IP.To4()) + } else if v6net := net1.IP.To16(); v6net != nil { + ipBigInt.SetBytes(net1.IP.To16()) + } + + ipBigInt.Add(ipBigInt, offset) + ip := net.IP(ipBigInt.Bytes()) + + return &PhantomIP{ip: &ip, supportRandomPort: net1.supportRandomPort}, nil +} diff --git a/pkg/station/lib/phantom_selector_test.go b/pkg/phantoms/phantom_selector_test.go similarity index 50% rename from pkg/station/lib/phantom_selector_test.go rename to pkg/phantoms/phantom_selector_test.go index b5247b5c..8f0394c8 100644 --- a/pkg/station/lib/phantom_selector_test.go +++ b/pkg/phantoms/phantom_selector_test.go @@ -1,18 +1,18 @@ -package lib +package phantoms import ( - "crypto/sha256" - "encoding/binary" + "crypto/rand" "encoding/hex" - "fmt" - "math/rand" "net" "os" "testing" + "github.com/refraction-networking/conjure/pkg/core" + pb "github.com/refraction-networking/conjure/proto" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/crypto/hkdf" + "google.golang.org/protobuf/proto" ) func TestPhantomsIPSelectionAlt(t *testing.T) { @@ -91,215 +91,175 @@ func TestPhantomsSelectFromUnknownGen(t *testing.T) { seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") - phantomAddr, err := phantomSelector.Select(seed, 0, phantomSelectionMinGeneration, false) + phantomAddr, err := phantomSelector.Select(seed, 0, core.PhantomSelectionMinGeneration, false) require.Equal(t, err.Error(), "generation number not recognized") assert.Nil(t, phantomAddr) } -func TestPhantomsSeededSelectionV4(t *testing.T) { +// This tests Client V2 +func TestPhantomsSeededSelectionHkdf(t *testing.T) { os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") phantomSelector, err := NewPhantomIPSelector() require.Nil(t, err, "Failed to create the PhantomIPSelector Object") var newConf = &SubnetConfig{ - WeightedSubnets: []ConjurePhantomSubnet{ - {Weight: 9, Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}, RandomizeDstPort: true}, - {Weight: 1, Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}, RandomizeDstPort: true}, + WeightedSubnets: []*pb.PhantomSubnets{ + {Weight: proto.Uint32(9), Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}}, + {Weight: proto.Uint32(1), Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}}, }, } newGen := phantomSelector.AddGeneration(-1, newConf) seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") - expectedAddr := "192.122.190.130" + expectedAddr := "192.122.190.164" - phantomAddr, err := phantomSelector.Select(seed, newGen, phantomSelectionMinGeneration, false) + phantomAddr, err := phantomSelector.Select(seed, newGen, 2, false) require.Nil(t, err) assert.Equal(t, expectedAddr, phantomAddr.String()) - } -// Client V1 -func TestPhantomsSeededSelectionV6Varint(t *testing.T) { +func TestPhantomsV6Hkdf(t *testing.T) { os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") phantomSelector, err := NewPhantomIPSelector() require.Nil(t, err, "Failed to create the PhantomIPSelector Object") var newConf = &SubnetConfig{ - WeightedSubnets: []ConjurePhantomSubnet{ - {Weight: 9, Subnets: []string{"192.122.190.0/24", "2001:48a8:687f:1::/64"}, RandomizeDstPort: true}, - {Weight: 1, Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}, RandomizeDstPort: true}, + WeightedSubnets: []*pb.PhantomSubnets{ + {Weight: proto.Uint32(9), Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}}, + {Weight: proto.Uint32(1), Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}}, }, } newGen := phantomSelector.AddGeneration(-1, newConf) seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") - expectedAddr := "2001:48a8:687f:1:5fa4:c34c:434e:ddd" - - phantomAddr, err := phantomSelector.Select(seed, newGen, 1, true) - require.Nil(t, err) - assert.Equal(t, expectedAddr, phantomAddr.String()) -} - -func TestPhantomsV6OnlyFilter(t *testing.T) { - testNets := &ConjurePhantomSubnet{1, []string{"192.122.190.0/24", "2001:48a8:687f:1::/64", "2001:48a8:687f:1::/64"}, true} - testNetsParsed, err := parseSubnets(testNets) - require.Nil(t, err) - require.Equal(t, 3, len(testNetsParsed)) - testNetsParsed, err = V6Only(testNetsParsed) + phantomAddr, err := phantomSelector.Select(seed, newGen, 2, true) require.Nil(t, err) - require.Equal(t, 2, len(testNetsParsed)) - + assert.True(t, phantomAddr.To4() == nil) + assert.True(t, phantomAddr.To16() != nil) } -// TestPhantomsSeededSelectionV4Min ensures that minimal subnets work because -// they re useful to test limitations (i.e. multiple clients sharing a phantom -// address) -func TestPhantomsSeededSelectionV4Min(t *testing.T) { - subnets, err := parseSubnets(&ConjurePhantomSubnet{1, []string{"192.122.190.0/32", "2001:48a8:687f:1::/128"}, true}) - require.Nil(t, err) - - seed, err := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") - require.Nil(t, err) - - phantomAddr, err := selectPhantomImplVarint(seed, subnets) - require.Nil(t, err) - - possibleAddrs := []string{"192.122.190.0", "2001:48a8:687f:1::"} - require.Contains(t, possibleAddrs, phantomAddr.String()) +var testSetOfWeightedSubnetLists = map[string][]*pb.PhantomSubnets{ + "plain case": { + {Weight: proto.Uint32(9), Subnets: []string{"10.0.0.0/8", "2001:48a8:687f:1::/64"}}, + {Weight: proto.Uint32(1), Subnets: []string{"141.219.0.0/16", "2001:1::/64"}}, + }, + "one set missing ipv6": { + {Weight: proto.Uint32(9), Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}}, + {Weight: proto.Uint32(1), Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}}, + }, + + "set with fewer v6 addreses than v4": { + {Weight: proto.Uint32(1), Subnets: []string{"10.0.0.0/8", "2001:48a8:687f:1::/120"}}, + }, } -// TestPhantomSeededSelectionFuzz ensures that all phantom subnet sizes are -// viable including small (/31, /32, etc.) subnets which were previously -// experiencing a divide by 0. -func TestPhantomSeededSelectionFuzz(t *testing.T) { - _, defaultV6, err := net.ParseCIDR("2001:48a8:687f:1::/64") - require.Nil(t, err) - - var randSeed int64 = 1234 - r := rand.New(rand.NewSource(randSeed)) - - // Add generation with only one v4 subnet that has a varying mask len - for i := 0; i <= 32; i++ { - s := "255.255.255.255/" + fmt.Sprint(i) - _, variableSubnet, err := net.ParseCIDR(s) - require.Nil(t, err) - - subnets := []*phantomNet{{defaultV6, true}, {variableSubnet, false}} - - var seed = make([]byte, 32) - for j := 0; j < 10000; j++ { - n, err := r.Read(seed) - require.Nil(t, err) - require.Equal(t, n, 32) - - // phantomAddr, err := phantomSelector.Select(seed, newGen, false) - phantomAddr, err := selectPhantomImplVarint(seed, subnets) - require.Nil(t, err, "i=%d, j=%d, seed='%s'", i, j, hex.EncodeToString(seed)) - require.NotNil(t, phantomAddr) - } - } -} - -// This tests Client V0 -func TestPhantomsSeededSelectionLegacy(t *testing.T) { +func TestPhantomsCompareClientAndStation(t *testing.T) { os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") phantomSelector, err := NewPhantomIPSelector() require.Nil(t, err, "Failed to create the PhantomIPSelector Object") + seed := make([]byte, 32) - var newConf = &SubnetConfig{ - WeightedSubnets: []ConjurePhantomSubnet{ - {Weight: 9, Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}}, - {Weight: 1, Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}}, - }, - } + for _, testCase := range testSetOfWeightedSubnetLists { + var psl = &pb.PhantomSubnetsList{WeightedSubnets: testCase} + var newConf = &SubnetConfig{WeightedSubnets: testCase} + newGen := phantomSelector.AddGeneration(-1, newConf) - newGen := phantomSelector.AddGeneration(-1, newConf) - - seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") - expectedAddr := "192.122.190.130" - - phantomAddr, err := phantomSelector.Select(seed, newGen, 0, false) - require.Nil(t, err) - assert.Equal(t, expectedAddr, phantomAddr.String()) - -} - -// This tests Client V1 -func TestPhantomsSeededSelectionVarint(t *testing.T) { - os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") - phantomSelector, err := NewPhantomIPSelector() - require.Nil(t, err, "Failed to create the PhantomIPSelector Object") + for i := 0; i < 10_000; i++ { + _, err := rand.Read(seed) + require.Nil(t, err) - var newConf = &SubnetConfig{ - WeightedSubnets: []ConjurePhantomSubnet{ - {Weight: 9, Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}}, - {Weight: 1, Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}}, - }, + clientAddr, clientErr := SelectPhantom(seed, psl, V4Only, true) + stationAddr, stationErr := phantomSelector.Select(seed, newGen, uint(core.CurrentClientLibraryVersion()), false) + if stationErr != nil && clientErr != nil { + require.Equal(t, clientErr.Error(), stationErr.Error()) + } else { + require.Nil(t, stationErr) + require.Nil(t, clientErr) + require.NotNil(t, clientAddr) + require.NotNil(t, stationAddr) + if stationAddr != nil && clientAddr != nil { + require.Equal(t, clientAddr.String(), stationAddr.String(), "client:%s, station:%s", clientAddr, stationAddr) + } + } + + clientAddr, clientErr = SelectPhantom(seed, psl, V6Only, true) + stationAddr, stationErr = phantomSelector.Select(seed, newGen, uint(core.CurrentClientLibraryVersion()), true) + if stationErr != nil && clientErr != nil { + require.Equal(t, clientErr.Error(), stationErr.Error()) + } else { + require.Nil(t, stationErr) + require.Nil(t, clientErr) + require.NotNil(t, clientAddr) + require.NotNil(t, stationAddr) + if stationAddr != nil && clientAddr != nil { + require.Equal(t, clientAddr.String(), stationAddr.String(), "client:%s, station:%s", clientAddr, stationAddr) + } + } + } } - - newGen := phantomSelector.AddGeneration(-1, newConf) - - seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") - expectedAddr := "192.122.190.130" - - phantomAddr, err := phantomSelector.Select(seed, newGen, 1, false) - require.Nil(t, err) - assert.Equal(t, expectedAddr, phantomAddr.String()) } -// This tests Client V2 -func TestPhantomsSeededSelectionHkdf(t *testing.T) { +func TestPhantomsCompareClientAndStationCount(t *testing.T) { os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") phantomSelector, err := NewPhantomIPSelector() require.Nil(t, err, "Failed to create the PhantomIPSelector Object") - var newConf = &SubnetConfig{ - WeightedSubnets: []ConjurePhantomSubnet{ - {Weight: 9, Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}}, - {Weight: 1, Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}}, - }, - } - - newGen := phantomSelector.AddGeneration(-1, newConf) - - seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") - expectedAddr := "192.122.190.164" - - phantomAddr, err := phantomSelector.Select(seed, newGen, 2, false) - require.Nil(t, err) - assert.Equal(t, expectedAddr, phantomAddr.String()) -} - -func TestPhantomsV6Hkdf(t *testing.T) { - os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") - phantomSelector, err := NewPhantomIPSelector() - require.Nil(t, err, "Failed to create the PhantomIPSelector Object") + seed := make([]byte, 32) + iterations := 10_000 + for _, testCase := range testSetOfWeightedSubnetLists { + var psl = &pb.PhantomSubnetsList{WeightedSubnets: testCase} + var newConf = &SubnetConfig{WeightedSubnets: testCase} + newGen := phantomSelector.AddGeneration(-1, newConf) + v4 := 0 + v6 := 0 + v4ClientErrs := 0 + v4StationErrs := 0 + v6ClientErrs := 0 + v6StationErrs := 0 + for i := 0; i < iterations; i++ { + _, err := rand.Read(seed) + require.Nil(t, err) - var newConf = &SubnetConfig{ - WeightedSubnets: []ConjurePhantomSubnet{ - {Weight: 9, Subnets: []string{"192.122.190.0/24", "10.0.0.0/31", "2001:48a8:687f:1::/64"}}, - {Weight: 1, Subnets: []string{"141.219.0.0/16", "35.8.0.0/16"}}, - }, + clientAddr, clientErr := SelectPhantom(seed, psl, V4Only, true) + stationAddr, stationErr := phantomSelector.Select(seed, newGen, uint(core.CurrentClientLibraryVersion()), false) + if stationErr != nil { + v4StationErrs++ + } + if clientErr != nil { + v4ClientErrs++ + } + if stationErr != nil && clientErr != nil && stationErr.Error() == clientErr.Error() { + v4++ + } + + if stationAddr != nil && clientAddr != nil && stationAddr.String() == clientAddr.String() { + v4++ + } + + clientAddr, clientErr = SelectPhantom(seed, psl, V6Only, true) + stationAddr, stationErr = phantomSelector.Select(seed, newGen, uint(core.CurrentClientLibraryVersion()), true) + if stationErr != nil { + v6StationErrs++ + } + if clientErr != nil { + v6ClientErrs++ + } + + if stationErr != nil && clientErr != nil && stationErr.Error() == clientErr.Error() { + v6++ + } + + if stationAddr != nil && clientAddr != nil && stationAddr.String() == clientAddr.String() { + v6++ + } + } + t.Log("V4: ", v4, "V6: ", v6, "V4ClientErrs: ", v4ClientErrs, "V4StationErrs: ", v4StationErrs, "V6ClientErrs: ", v6ClientErrs, "V6StationErrs: ", v6StationErrs) + require.Equal(t, iterations, v4) + require.Equal(t, iterations, v6) } - - newGen := phantomSelector.AddGeneration(-1, newConf) - - seed, _ := hex.DecodeString("5a87133b68ea3468988a21659a12ed2ece07345c8c1a5b08459ffdea4218d12f") - //expectedAddr := "2001:48a8:687f:1:5fa4:c34c:434e:ddd" - expectedAddr := "2001:48a8:687f:1:d8f4:45cd:3ae:fcd4" - - phantomAddr, err := phantomSelector.Select(seed, newGen, 2, true) - require.Nil(t, err) - assert.Equal(t, expectedAddr, phantomAddr.String()) -} - -func ExpandSeed(seed, salt []byte, i int) []byte { - bi := make([]byte, 8) - binary.LittleEndian.PutUint64(bi, uint64(i)) - return hkdf.Extract(sha256.New, seed, append(salt, bi...)) } // TestDuplicates demonstrates that selectPhantomImplVarint results in @@ -322,9 +282,9 @@ func TestDuplicates(t *testing.T) { require.Nil(t, err, "Failed to create the PhantomIPSelector Object") var newConf = &SubnetConfig{ - WeightedSubnets: []ConjurePhantomSubnet{ - {Weight: 1, Subnets: []string{"2001:48a8:687f:1::/64"}}, - {Weight: 9, Subnets: []string{"2002::/64"}}, + WeightedSubnets: []*pb.PhantomSubnets{ + {Weight: proto.Uint32(1), Subnets: []string{"2001:48a8:687f:1::/64"}}, + {Weight: proto.Uint32(9), Subnets: []string{"2002::/64"}}, }, } diff --git a/pkg/phantoms/phantoms.go b/pkg/phantoms/phantoms.go index 16e513b7..7d64c717 100644 --- a/pkg/phantoms/phantoms.go +++ b/pkg/phantoms/phantoms.go @@ -1,16 +1,10 @@ package phantoms import ( - "crypto/rand" - "crypto/sha256" - "errors" "fmt" - "math/big" "net" - "sort" pb "github.com/refraction-networking/conjure/proto" - "golang.org/x/crypto/hkdf" ) type phantomNet struct { @@ -22,67 +16,15 @@ func (p *phantomNet) SupportRandomPort() bool { return p.supportRandomPort } -// getSubnets - return EITHER all subnet strings as one composite array if we are -// -// selecting unweighted, or return the array associated with the (seed) selected -// array of subnet strings based on the associated weights -func getSubnets(sc *pb.PhantomSubnetsList, seed []byte, weighted bool) ([]*phantomNet, error) { - weightedSubnets := sc.GetWeightedSubnets() - if weightedSubnets == nil { - return []*phantomNet{}, nil - } - - if weighted { - choices := make([]*pb.PhantomSubnets, 0, len(weightedSubnets)) - - totWeight := int64(0) - for _, cjSubnet := range weightedSubnets { - weight := cjSubnet.GetWeight() - subnets := cjSubnet.GetSubnets() - if subnets == nil { - continue - } - - totWeight += int64(weight) - choices = append(choices, cjSubnet) - } - - // Sort choices assending - sort.Slice(choices, func(i, j int) bool { - return choices[i].GetWeight() < choices[j].GetWeight() - }) - - // Naive method: get random int, subtract from weights until you are < 0 - hkdfReader := hkdf.New(sha256.New, seed, nil, []byte("phantom-select-subnet")) - totWeightBig := big.NewInt(totWeight) - rndBig, err := rand.Int(hkdfReader, totWeightBig) - if err != nil { - return nil, err - } - - // Decrement rnd by each weight until it's < 0 - rnd := rndBig.Int64() - for _, choice := range choices { - rnd -= int64(choice.GetWeight()) - if rnd < 0 { - return parseSubnets(choice) - } - } - - return []*phantomNet{}, nil - } - - // Use unweighted config for subnets, concat all into one array and return. - out := []*phantomNet{} - for _, cjSubnet := range weightedSubnets { - nets, err := parseSubnets(cjSubnet) - if err != nil { - return nil, fmt.Errorf("error parsing subnet: %v", err) - } - out = append(out, nets...) - } +type genericSubnetConfig interface { + GetWeightedSubnets() []*pb.PhantomSubnets +} - return out, nil +// getSubnets - return EITHER all subnet strings as one composite array if we are +// selecting unweighted, or return the array associated with the (seed) selected +// array of subnet strings based on the associated weights +func getSubnets(sc genericSubnetConfig, seed []byte, weighted bool) ([]*phantomNet, error) { + return getSubnetsHkdf(sc, seed, weighted) } // SubnetFilter - Filter IP subnets based on whatever to prevent specific subnets from @@ -149,103 +91,13 @@ func parseSubnet(phantomSubnet string) (*net.IPNet, error) { return parsedNet, nil } -// SelectAddrFromSubnetOffset given a CIDR block and offset, return the net.IP -func SelectAddrFromSubnetOffset(net1 *phantomNet, offset *big.Int) (*PhantomIP, error) { - bits, addrLen := net1.Mask.Size() - - // Compute network size (e.g. an ipv4 /24 is 2^(32-24) - var netSize big.Int - netSize.Exp(big.NewInt(2), big.NewInt(int64(addrLen-bits)), nil) - - // Check that offset is within this subnet - if netSize.Cmp(offset) <= 0 { - return nil, errors.New("offset too big for subnet") - } - - ipBigInt := &big.Int{} - if v4net := net1.IP.To4(); v4net != nil { - ipBigInt.SetBytes(net1.IP.To4()) - } else if v6net := net1.IP.To16(); v6net != nil { - ipBigInt.SetBytes(net1.IP.To16()) - } - - ipBigInt.Add(ipBigInt, offset) - ip := net.IP(ipBigInt.Bytes()) - - return &PhantomIP{ip: &ip, supportRandomPort: net1.supportRandomPort}, nil -} - // selectIPAddr selects an ip address from the list of subnets associated // with the specified generation by constructing a set of start and end values // for the high and low values in each allocation. The random number is then // bound between the global min and max of that set. This ensures that // addresses are chosen based on the number of addresses in the subnet. func selectIPAddr(seed []byte, subnets []*phantomNet) (*PhantomIP, error) { - type idNet struct { - min, max big.Int - net *phantomNet - } - var idNets []idNet - - // Compose a list of ID Nets with min, max and network associated and count - // the total number of available addresses. - addressTotal := big.NewInt(0) - for _, _net := range subnets { - netMaskOnes, _ := _net.Mask.Size() - if ipv4net := _net.IP.To4(); ipv4net != nil { - _idNet := idNet{} - _idNet.min.Set(addressTotal) - addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(32-netMaskOnes)), nil)) - _idNet.max.Sub(addressTotal, big.NewInt(1)) - _idNet.net = _net - idNets = append(idNets, _idNet) - } else if ipv6net := _net.IP.To16(); ipv6net != nil { - _idNet := idNet{} - _idNet.min.Set(addressTotal) - addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(128-netMaskOnes)), nil)) - _idNet.max.Sub(addressTotal, big.NewInt(1)) - _idNet.net = _net - idNets = append(idNets, _idNet) - } else { - return nil, fmt.Errorf("failed to parse %v", _net) - } - } - - // If the total number of addresses is 0 something has gone wrong - if addressTotal.Cmp(big.NewInt(0)) <= 0 { - return nil, fmt.Errorf("no valid addresses specified") - } - - // Pick a value using the seed in the range of between 0 and the total - // number of addresses. - hkdfReader := hkdf.New(sha256.New, seed, nil, []byte("phantom-addr-id")) - id, err := rand.Int(hkdfReader, addressTotal) - if err != nil { - return nil, err - } - - // Find the network (ID net) that contains our random value and select a - // random address from that subnet. - // min >= id%total >= max - var result *PhantomIP - for _, _idNet := range idNets { - // fmt.Printf("tot:%s, seed%%tot:%s id cmp max: %d, id cmp min: %d %s\n", addressTotal.String(), id, _idNet.max.Cmp(id), _idNet.min.Cmp(id), _idNet.net.String()) - if _idNet.max.Cmp(id) >= 0 && _idNet.min.Cmp(id) <= 0 { - - var offset big.Int - offset.Sub(id, &_idNet.min) - result, err = SelectAddrFromSubnetOffset(_idNet.net, &offset) - if err != nil { - return nil, fmt.Errorf("failed to chose IP address: %v", err) - } - } - } - - // We want to make it so this CANNOT happen - if result == nil { - return nil, errors.New("nil result should not be possible") - } - return result, nil + return selectPhantomImplHkdf(seed, subnets) } // SelectPhantom - select one phantom IP address based on shared secret @@ -300,7 +152,11 @@ func GetUnweightedSubnetList(subnetsList *pb.PhantomSubnetsList) ([]*phantomNet, return getSubnets(subnetsList, nil, false) } -// type aliase to make embedding unexported +func IP(ip net.IP, supportRandomPort bool) *PhantomIP { + return &PhantomIP{ip: &ip, supportRandomPort: supportRandomPort} +} + +// type alias to make embedding unexported // nolint:unused type ip = net.IP type PhantomIP struct { diff --git a/pkg/phantoms/phantoms_test.go b/pkg/phantoms/phantoms_test.go index 5fb33eeb..5fff6bbf 100644 --- a/pkg/phantoms/phantoms_test.go +++ b/pkg/phantoms/phantoms_test.go @@ -24,7 +24,7 @@ func TestIPSelectionBasic(t *testing.T) { _, net1, err := net.ParseCIDR(netStr) require.Nil(t, err) - addr, err := SelectAddrFromSubnetOffset(&phantomNet{IPNet: net1}, offset) + addr, err := selectAddrFromSubnetOffset(&phantomNet{IPNet: net1}, offset) require.Nil(t, err) //require.Equal(t, "2001:48a8:687f:1:5fa4:c34c:434e:ddd", addr.String()) require.Equal(t, "2001:48a8:687f:1:7ead:beef:cafe:d00d", addr.String()) @@ -38,14 +38,14 @@ func TestOffsetTooLarge(t *testing.T) { require.Nil(t, err) // Offset too big - addr, err := SelectAddrFromSubnetOffset(&phantomNet{IPNet: net1}, offset) + addr, err := selectAddrFromSubnetOffset(&phantomNet{IPNet: net1}, offset) if err == nil { t.Fatalf("Error: expected error, got address %v", addr) } // Offset that is just fine offset = big.NewInt(255) - addr, err = SelectAddrFromSubnetOffset(&phantomNet{IPNet: net1}, offset) + addr, err = selectAddrFromSubnetOffset(&phantomNet{IPNet: net1}, offset) require.Nil(t, err) require.Equal(t, "10.1.2.255", addr.String()) } diff --git a/pkg/phantoms/station_phantoms.go b/pkg/phantoms/station_phantoms.go new file mode 100644 index 00000000..ebdb8d6b --- /dev/null +++ b/pkg/phantoms/station_phantoms.go @@ -0,0 +1,193 @@ +package phantoms + +import ( + "fmt" + "os" + "strconv" + + toml "github.com/pelletier/go-toml" + "github.com/refraction-networking/conjure/pkg/core" + pb "github.com/refraction-networking/conjure/proto" +) + +// SubnetConfig - Configuration of subnets for Conjure to choose a Phantom out of. +type SubnetConfig struct { + WeightedSubnets []*pb.PhantomSubnets +} + +func (sc *SubnetConfig) GetWeightedSubnets() []*pb.PhantomSubnets { + return sc.WeightedSubnets +} + +// PhantomIPSelector - Object for tracking current generation to SubnetConfig Mapping. +type PhantomIPSelector struct { + Networks map[uint]*SubnetConfig +} + +// type shim because github.com/pelletier/go-toml doesn't allow for integer value keys to maps so +// we have to parse them ourselves. :( +type phantomIPSelectorInternal struct { + Networks map[string]*SubnetConfig +} + +// NewPhantomIPSelector - create object currently populated with a static map of +// generation number to SubnetConfig, but this may be loaded dynamically in the +// future. +func NewPhantomIPSelector() (*PhantomIPSelector, error) { + return GetPhantomSubnetSelector() +} + +// GetPhantomSubnetSelector gets the location of the configuration file from an +// environment variable and returns the parsed configuration. +func GetPhantomSubnetSelector() (*PhantomIPSelector, error) { + return SubnetsFromTomlFile(os.Getenv("PHANTOM_SUBNET_LOCATION")) +} + +// SubnetsFromTomlFile takes a path and parses the toml config file +func SubnetsFromTomlFile(path string) (*PhantomIPSelector, error) { + + tree, err := toml.LoadFile(path) + if err != nil { + return nil, fmt.Errorf("error opening configuration file: %v", err) + } + + var pss = &PhantomIPSelector{ + Networks: make(map[uint]*SubnetConfig), + } + // shim because github.com/pelletier/go-toml doesn't allow for integer value keys to maps so + // we have to parse them ourselves. :( + var phantomSelectorSet = &phantomIPSelectorInternal{} + err = tree.Unmarshal(phantomSelectorSet) + if err != nil { + return nil, fmt.Errorf("error unmarshalling configuration file: %v", err) + } + + for gen, set := range phantomSelectorSet.Networks { + g, err := strconv.Atoi(gen) + if err != nil { + return nil, err + } + // fmt.Printf("[GetPhantomSubnetSelector] adding %d, %+v\n", g, set) + pss.AddGeneration(g, set) + } + + return pss, nil +} + +// GetSubnetsByGeneration - provide a generation index. If the generation exists +// the associated SubnetConfig is returned. If it is not defined the default +// subnets are returned. +func (p *PhantomIPSelector) GetSubnetsByGeneration(generation uint) *SubnetConfig { + if subnets, ok := p.Networks[generation]; ok { + return subnets + } + + // No Default subnets provided if the generation is not known + return nil +} + +// AddGeneration - add a subnet config as a new new generation, if the requested +// generation index is taken then it uses (and returns) the next available +// number. +func (p *PhantomIPSelector) AddGeneration(gen int, subnets *SubnetConfig) uint { + + ugen := uint(gen) + + if gen == -1 || p.IsTakenGeneration(ugen) { + ugen = p.newGenerationIndex() + } + + p.Networks[ugen] = subnets + return ugen +} + +func (p *PhantomIPSelector) newGenerationIndex() uint { + maxGen := uint(0) + for k := range p.Networks { + if k > maxGen { + maxGen = k + } + } + return maxGen + 1 +} + +// IsTakenGeneration - check if the generation index is already in use. +func (p *PhantomIPSelector) IsTakenGeneration(gen uint) bool { + if _, ok := p.Networks[gen]; ok { + return true + } + return false +} + +// RemoveGeneration - remove a generation from the mapping +func (p *PhantomIPSelector) RemoveGeneration(generation uint) bool { + p.Networks[generation] = nil + return true +} + +// UpdateGeneration - Update the subnet list associated with a specific generation +func (p *PhantomIPSelector) UpdateGeneration(generation uint, subnets *SubnetConfig) bool { + p.Networks[generation] = subnets + return true +} + +// Select - select an ip address from the list of subnets associated with the specified generation +func (p *PhantomIPSelector) Select(seed []byte, generation uint, clientLibVer uint, v6Support bool) (*PhantomIP, error) { + genConfig := p.GetSubnetsByGeneration(generation) + if genConfig == nil { + return nil, fmt.Errorf("generation number not recognized") + } + + genSubnets, err := subnetsByVersion(seed, clientLibVer, genConfig) + if err != nil { + return nil, err + } + + if v6Support { + genSubnets, err = V6Only(genSubnets) + if err != nil { + return nil, err + } + } else { + genSubnets, err = V4Only(genSubnets) + if err != nil { + return nil, err + } + } + + // handle legacy clientLibVersions for selecting phantoms. + if clientLibVer < core.PhantomSelectionMinGeneration { + // Version 0 + ip, err := selectPhantomImplV0(seed, genSubnets) + if err != nil { + return nil, err + } + return ip, nil + } else if clientLibVer < core.PhantomHkdfMinVersion { + // Version 1 + ip, err := selectPhantomImplVarint(seed, genSubnets) + if err != nil { + return nil, err + } + return ip, nil + } + + // Version 2+ + ip, err := selectPhantomImplHkdf(seed, genSubnets) + if err != nil { + return nil, err + } + return ip, nil +} + +func subnetsByVersion(seed []byte, clientLibVer uint, genConfig *SubnetConfig) ([]*phantomNet, error) { + + if clientLibVer < core.PhantomHkdfMinVersion { + // Version 0 or 1 + return genConfig.getSubnetsVarint(seed, true) + } else { + // Version 2 + return getSubnetsHkdf(genConfig, seed, true) + } + +} diff --git a/pkg/phantoms/test/phantom_subnets.toml b/pkg/phantoms/test/phantom_subnets.toml new file mode 100644 index 00000000..ee687c06 --- /dev/null +++ b/pkg/phantoms/test/phantom_subnets.toml @@ -0,0 +1,25 @@ + + +[Networks] + [Networks.1] + Generation = 1 + [[Networks.1.WeightedSubnets]] + Weight = 9 + Subnets = ["192.122.190.0/24", "2001:48a8:687f:1::/64"] + + [Networks.2] + Generation = 2 + [[Networks.2.WeightedSubnets]] + Weight = 1 + Subnets = ["192.122.190.0/28", "2001:48a8:687f:1::/96"] + + [Networks.957] + Generation = 957 + [[Networks.957.WeightedSubnets]] + Weight = 9 + RandomizeDstPort = true + Subnets = ["192.122.190.0/24", "2001:48a8:687f:1::/64"] + [[Networks.957.WeightedSubnets]] + Weight = 1 + RandomizeDstPort = false + Subnets = ["141.219.0.0/16", "35.8.0.0/16"] diff --git a/pkg/phantoms/test/phantom_subnets_update.toml b/pkg/phantoms/test/phantom_subnets_update.toml new file mode 100644 index 00000000..9e0668dd --- /dev/null +++ b/pkg/phantoms/test/phantom_subnets_update.toml @@ -0,0 +1,32 @@ + + +[Networks] + [Networks.1] + Generation = 1 + [[Networks.1.WeightedSubnets]] + Weight = 9 + Subnets = ["192.122.190.0/24", "2001:48a8:687f:1::/64"] + + [Networks.2] + Generation = 2 + [[Networks.2.WeightedSubnets]] + Weight = 1 + Subnets = ["192.122.190.0/28", "2001:48a8:687f:1::/96"] + + [Networks.957] + Generation = 957 + [[Networks.957.WeightedSubnets]] + Weight = 9 + Subnets = ["192.122.190.0/24", "2001:48a8:687f:1::/64"] + [[Networks.957.WeightedSubnets]] + Weight = 1 + Subnets = ["141.219.0.0/16", "35.8.0.0/16"] + + [Networks.1000] + Generation = 1000 + [[Networks.1000.WeightedSubnets]] + Weight = 9 + Subnets = ["192.168.10.0/24", "2001:1::/64"] + [[Networks.1000.WeightedSubnets]] + Weight = 1 + Subnets = ["10.0.0.0/16"] diff --git a/pkg/regserver/apiregserver/apiregserver.go b/pkg/regserver/apiregserver/apiregserver.go index 1634f016..496a698d 100644 --- a/pkg/regserver/apiregserver/apiregserver.go +++ b/pkg/regserver/apiregserver/apiregserver.go @@ -12,8 +12,8 @@ import ( "github.com/gorilla/mux" "github.com/refraction-networking/conjure/pkg/metrics" + "github.com/refraction-networking/conjure/pkg/phantoms" "github.com/refraction-networking/conjure/pkg/regserver/regprocessor" - "github.com/refraction-networking/conjure/pkg/station/lib" pb "github.com/refraction-networking/conjure/proto" log "github.com/sirupsen/logrus" "google.golang.org/protobuf/proto" @@ -221,7 +221,7 @@ func (s *APIRegServer) registerBidirectional(w http.ResponseWriter, r *http.Requ switch err { case regprocessor.ErrNoC2SBody: http.Error(w, "no C2S body", http.StatusBadRequest) - case lib.ErrLegacyAddrSelectBug: + case phantoms.ErrLegacyAddrSelectBug: http.Error(w, "bad seed", http.StatusBadRequest) default: reqLogger.Errorf("failed to create registration response: %v", err) diff --git a/pkg/regserver/regprocessor/regprocessor.go b/pkg/regserver/regprocessor/regprocessor.go index 848f0d1b..278d6dd3 100644 --- a/pkg/regserver/regprocessor/regprocessor.go +++ b/pkg/regserver/regprocessor/regprocessor.go @@ -17,6 +17,7 @@ import ( "github.com/refraction-networking/conjure/pkg/core" "github.com/refraction-networking/conjure/pkg/core/interfaces" "github.com/refraction-networking/conjure/pkg/metrics" + "github.com/refraction-networking/conjure/pkg/phantoms" "github.com/refraction-networking/conjure/pkg/regserver/overrides" "github.com/refraction-networking/conjure/pkg/station/lib" pb "github.com/refraction-networking/conjure/proto" @@ -57,7 +58,7 @@ type zmqSender interface { } type ipSelector interface { - Select([]byte, uint, uint, bool) (*lib.PhantomIP, error) + Select([]byte, uint, uint, bool) (*phantoms.PhantomIP, error) } // RegProcessor provides an interface to publish registrations and helper functions to process registration requests @@ -83,7 +84,7 @@ func NewRegProcessor(zmqBindAddr string, zmqPort uint16, privkey []byte, authVer return nil, fmt.Errorf("incorrect private key size %d, expected %d", len(privkey), ed25519.PrivateKeySize) } - phantomSelector, err := lib.GetPhantomSubnetSelector() + phantomSelector, err := phantoms.GetPhantomSubnetSelector() if err != nil { return nil, err } @@ -159,7 +160,7 @@ func NewRegProcessorNoAuth(zmqBindAddr string, zmqPort uint16, metrics *metrics. return nil, ErrZmqSocket } - phantomSelector, err := lib.GetPhantomSubnetSelector() + phantomSelector, err := phantoms.GetPhantomSubnetSelector() if err != nil { return nil, err } @@ -293,7 +294,7 @@ func (p *RegProcessor) processBdReq(c2sPayload *pb.C2SWrapper) (*pb.Registration addr4 := binary.BigEndian.Uint32(phantom4.To4()) regResp.Ipv4Addr = &addr4 - phantomSubnetSupportsRandPort = phantom4.SupportsPortRand + phantomSubnetSupportsRandPort = phantom4.SupportRandomPort() } if c2s.GetV6Support() { @@ -309,8 +310,8 @@ func (p *RegProcessor) processBdReq(c2sPayload *pb.C2SWrapper) (*pb.Registration return nil, err } - regResp.Ipv6Addr = *phantom6.IP - phantomSubnetSupportsRandPort = phantom6.SupportsPortRand + regResp.Ipv6Addr = *phantom6.IP() + phantomSubnetSupportsRandPort = phantom6.SupportRandomPort() } transportType := c2s.GetTransport() @@ -418,7 +419,7 @@ func (p *RegProcessor) processC2SWrapper(c2sPayload *pb.C2SWrapper, clientAddr [ // subnets when the registrar receives a SIGHUP signal for example. If it fails it reports and error // and keeps the existing set of phantom subnets. func (p *RegProcessor) ReloadSubnets() error { - phantomSelector, err := lib.GetPhantomSubnetSelector() + phantomSelector, err := phantoms.GetPhantomSubnetSelector() if err != nil { return err } diff --git a/pkg/regserver/regprocessor/regprocessor_test.go b/pkg/regserver/regprocessor/regprocessor_test.go index 931e2458..ea4460f7 100644 --- a/pkg/regserver/regprocessor/regprocessor_test.go +++ b/pkg/regserver/regprocessor/regprocessor_test.go @@ -16,8 +16,8 @@ import ( "github.com/refraction-networking/conjure/pkg/core" "github.com/refraction-networking/conjure/pkg/core/interfaces" "github.com/refraction-networking/conjure/pkg/metrics" + "github.com/refraction-networking/conjure/pkg/phantoms" "github.com/refraction-networking/conjure/pkg/regserver/overrides" - "github.com/refraction-networking/conjure/pkg/station/lib" "github.com/refraction-networking/conjure/pkg/transports" "github.com/refraction-networking/conjure/pkg/transports/wrapping/min" "github.com/refraction-networking/conjure/pkg/transports/wrapping/prefix" @@ -305,11 +305,11 @@ type fakeIPSelector struct { v6Addr net.IP } -func (f fakeIPSelector) Select(seed []byte, generation uint, clientLibVer uint, v6Support bool) (*lib.PhantomIP, error) { +func (f fakeIPSelector) Select(seed []byte, generation uint, clientLibVer uint, v6Support bool) (*phantoms.PhantomIP, error) { if v6Support { - return &lib.PhantomIP{IP: &f.v6Addr, SupportsPortRand: true}, nil + return phantoms.IP(f.v6Addr, true), nil } - return &lib.PhantomIP{IP: &f.v4Addr, SupportsPortRand: true}, nil + return phantoms.IP(f.v4Addr, true), nil } func TestRegisterBidirectional(t *testing.T) { @@ -423,9 +423,9 @@ func TestRegProcessBdReq(t *testing.T) { type mockIPSelector struct{} -func (*mockIPSelector) Select([]byte, uint, uint, bool) (*lib.PhantomIP, error) { +func (*mockIPSelector) Select([]byte, uint, uint, bool) (*phantoms.PhantomIP, error) { ip := net.ParseIP("8.8.8.8") - return &lib.PhantomIP{IP: &ip, SupportsPortRand: true}, nil + return phantoms.IP(ip, true), nil } func TestRegProcessBdReqOverride(t *testing.T) { diff --git a/pkg/station/lib/phantom_selector.go b/pkg/station/lib/phantom_selector.go deleted file mode 100644 index edbceb61..00000000 --- a/pkg/station/lib/phantom_selector.go +++ /dev/null @@ -1,616 +0,0 @@ -package lib - -import ( - "crypto/rand" - "crypto/sha256" - "encoding/binary" - "errors" - "fmt" - "math/big" - mrand "math/rand" - "net" - "sort" - "time" - - wr "github.com/mroth/weightedrand" - "golang.org/x/crypto/hkdf" -) - -const ( - phantomSelectionMinGeneration uint = 1 - phantomHkdfMinVersion uint = 2 -) - -var ( - // ErrLegacyAddrSelectBug indicates that we have hit a corner case in a legacy address selection - // algorithm that causes phantom address selection to fail. - ErrLegacyAddrSelectBug = errors.New("no valid addresses specified") - // ErrMissingAddrs indicates that no subnets were provided with addresses to select from. This - // is only valid for phantomHkdfMinVersion and newer. - ErrMissingAddrs = errors.New("no valid addresses specified to select") -) - -// getSubnetsVarint - return EITHER all subnet strings as one composite array if -// we are selecting unweighted, or return the array associated with the (seed) -// selected array of subnet strings based on the associated weights -// -// Used by Client version 0 and 1 -func (sc *SubnetConfig) getSubnetsVarint(seed []byte, weighted bool) ([]*phantomNet, error) { - - if weighted { - // seed random with hkdf derived seed provided by client - seedInt, n := binary.Varint(seed) - if n == 0 { - return nil, fmt.Errorf("failed to seed random for weighted rand") - } - - // nolint:staticcheck // here for backwards compatibility with clients - mrand.Seed(seedInt) - - choices := make([]wr.Choice, 0, len(sc.WeightedSubnets)) - for _, cjSubnet := range sc.WeightedSubnets { - cjSubnet := cjSubnet // copy loop ptr - choices = append(choices, wr.Choice{Item: &cjSubnet, Weight: uint(cjSubnet.Weight)}) - } - c, err := wr.NewChooser(choices...) - if err != nil { - return nil, err - } - - return parseSubnets(c.Pick().(*ConjurePhantomSubnet)) - - } - - // Use unweighted config for subnets, concat all into one array and return. - out := []*phantomNet{} - for _, cjSubnet := range sc.WeightedSubnets { - nets, err := parseSubnets(&cjSubnet) - if err != nil { - return nil, fmt.Errorf("error parsing subnet: %v", err) - } - out = append(out, nets...) - } - - return out, nil -} - -// getSubnetsHkdf returns EITHER all subnet strings as one composite array if -// we are selecting unweighted, or return the array associated with the (seed) -// selected array of subnet strings based on the associated weights. Random -// values are seeded using an hkdf function to prevent biases introduced by -// math/rand and varint. -// -// Used by Client version 2+ -func (sc *SubnetConfig) getSubnetsHkdf(seed []byte, weighted bool) ([]*phantomNet, error) { - - weightedSubnets := sc.WeightedSubnets - if weightedSubnets == nil { - return []*phantomNet{}, nil - } - - if weighted { - choices := make([]*ConjurePhantomSubnet, 0, len(weightedSubnets)) - - totWeight := int64(0) - for _, cjSubnet := range weightedSubnets { - cjSubnet := cjSubnet // copy loop ptr - weight := cjSubnet.Weight - subnets := cjSubnet.Subnets - if subnets == nil { - continue - } - - totWeight += int64(weight) - choices = append(choices, &cjSubnet) - } - - // Sort choices assending - sort.Slice(choices, func(i, j int) bool { - return choices[i].Weight < choices[j].Weight - }) - - // Naive method: get random int, subtract from weights until you are < 0 - hkdfReader := hkdf.New(sha256.New, seed, nil, []byte("phantom-select-subnet")) - totWeightBig := big.NewInt(totWeight) - rndBig, err := rand.Int(hkdfReader, totWeightBig) - if err != nil { - return nil, err - } - - // Decrement rnd by each weight until it's < 0 - rnd := rndBig.Int64() - for _, choice := range choices { - rnd -= int64(choice.Weight) - if rnd < 0 { - return parseSubnets(choice) - } - } - - } - - // Use unweighted config for subnets, concat all into one array and return. - out := []*phantomNet{} - for _, cjSubnet := range weightedSubnets { - nets, err := parseSubnets(&cjSubnet) - if err != nil { - return nil, fmt.Errorf("error parsing subnet: %v", err) - } - out = append(out, nets...) - } - - return out, nil -} - -// SubnetFilter - Filter IP subnets based on whatever to prevent specific -// subnets from inclusion in choice. See v4Only and v6Only for reference. -type SubnetFilter func([]*net.IPNet) ([]*net.IPNet, error) - -// V4Only - a functor for transforming the subnet list to only include IPv4 subnets -func V4Only(obj []*phantomNet) ([]*phantomNet, error) { - out := []*phantomNet{} - - for _, _net := range obj { - if ipv4net := _net.IP.To4(); ipv4net != nil { - out = append(out, _net) - } - } - return out, nil -} - -// V6Only - a functor for transforming the subnet list to only include IPv6 subnets -func V6Only(obj []*phantomNet) ([]*phantomNet, error) { - out := []*phantomNet{} - - for _, _net := range obj { - if _net.IP == nil { - continue - } - if net := _net.IP.To4(); net != nil { - continue - } - out = append(out, _net) - } - return out, nil -} - -func parseSubnets(phantomSubnets *ConjurePhantomSubnet) ([]*phantomNet, error) { - subnets := []*phantomNet{} - - if len(phantomSubnets.Subnets) == 0 { - return nil, fmt.Errorf("parseSubnets - no subnets provided") - } - - for _, strNet := range phantomSubnets.Subnets { - _, parsedNet, err := net.ParseCIDR(strNet) - if err != nil { - return nil, err - } - if parsedNet == nil { - return nil, fmt.Errorf("failed to parse %v as subnet", parsedNet) - } - - subnets = append(subnets, &phantomNet{IPNet: parsedNet, supportRandomPort: phantomSubnets.RandomizeDstPort}) - } - - return subnets, nil -} - -// NewPhantomIPSelector - create object currently populated with a static map of -// generation number to SubnetConfig, but this may be loaded dynamically in the -// future. -func NewPhantomIPSelector() (*PhantomIPSelector, error) { - return GetPhantomSubnetSelector() -} - -// PhantomIP provides a wrapper around net.IP that can be used as a net.IP, while also indicating -// whether or not the subnet from which the address was selected supports port randomization. -type PhantomIP struct { - *net.IP - SupportsPortRand bool -} - -type phantomNet struct { - *net.IPNet - supportRandomPort bool -} - -func subnetsByVersion(seed []byte, clientLibVer uint, genConfig *SubnetConfig) ([]*phantomNet, error) { - - if clientLibVer < phantomHkdfMinVersion { - // Version 0 or 1 - return genConfig.getSubnetsVarint(seed, true) - } else { - // Version 2 - return genConfig.getSubnetsHkdf(seed, true) - } - -} - -// Select - select an ip address from the list of subnets associated with the specified generation -func (p *PhantomIPSelector) Select(seed []byte, generation uint, clientLibVer uint, v6Support bool) (*PhantomIP, error) { - genConfig := p.GetSubnetsByGeneration(generation) - if genConfig == nil { - return nil, fmt.Errorf("generation number not recognized") - } - - genSubnets, err := subnetsByVersion(seed, clientLibVer, genConfig) - if err != nil { - return nil, err - } - - if !v6Support { - genSubnets, err = V4Only(genSubnets) - if err != nil { - return nil, err - } - } - - // handle legacy clientLibVersions for selecting phantoms. - if clientLibVer < phantomSelectionMinGeneration { - // Version 0 - ip, err := selectPhantomImplV0(seed, genSubnets) - if err != nil { - return nil, err - } - return ip, nil - } else if clientLibVer < phantomHkdfMinVersion { - // Version 1 - ip, err := selectPhantomImplVarint(seed, genSubnets) - if err != nil { - return nil, err - } - return ip, nil - } - - // Version 2+ - ip, err := selectPhantomImplHkdf(seed, genSubnets) - if err != nil { - return nil, err - } - return ip, nil -} - -// selectPhantomImplVarint - select an ip address from the list of subnets -// associated with the specified generation by constructing a set of start and -// end values for the high and low values in each allocation. The random number -// is then bound between the global min and max of that set. This ensures that -// addresses are chosen based on the number of addresses in the subnet. -func selectPhantomImplVarint(seed []byte, subnets []*phantomNet) (*PhantomIP, error) { - type idNet struct { - min, max big.Int - net *phantomNet - } - var idNets []idNet - - // Compose a list of ID Nets with min, max and network associated and count - // the total number of available addresses. - addressTotal := big.NewInt(0) - for _, _net := range subnets { - netMaskOnes, _ := _net.Mask.Size() - if ipv4net := _net.IP.To4(); ipv4net != nil { - _idNet := idNet{} - _idNet.min.Set(addressTotal) - addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(32-netMaskOnes)), nil)) - _idNet.max.Sub(addressTotal, big.NewInt(1)) - _idNet.net = _net - idNets = append(idNets, _idNet) - } else if ipv6net := _net.IP.To16(); ipv6net != nil { - _idNet := idNet{} - _idNet.min.Set(addressTotal) - addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(128-netMaskOnes)), nil)) - _idNet.max.Sub(addressTotal, big.NewInt(1)) - _idNet.net = _net - idNets = append(idNets, _idNet) - } else { - return nil, fmt.Errorf("failed to parse %v", _net) - } - } - - // If the total number of addresses is 0 something has gone wrong - if addressTotal.Cmp(big.NewInt(0)) <= 0 { - return nil, ErrLegacyAddrSelectBug - } - - // Pick a value using the seed in the range of between 0 and the total - // number of addresses. - id := &big.Int{} - id.SetBytes(seed) - if id.Cmp(addressTotal) >= 0 { - id.Mod(id, addressTotal) - } - - // Find the network (ID net) that contains our random value and select a - // random address from that subnet. - // min >= id%total >= max - var result *PhantomIP - for _, _idNet := range idNets { - // fmt.Printf("tot:%s, seed%%tot:%s id cmp max: %d, id cmp min: %d %s\n", addressTotal.String(), id, _idNet.max.Cmp(id), _idNet.min.Cmp(id), _idNet.net.String()) - if _idNet.max.Cmp(id) >= 0 && _idNet.min.Cmp(id) <= 0 { - res, err := SelectAddrFromSubnet(seed, _idNet.net.IPNet) - if err != nil { - return nil, fmt.Errorf("failed to chose IP address: %v", err) - } - - result = &PhantomIP{IP: &res, SupportsPortRand: _idNet.net.supportRandomPort} - } - } - - // We want to make it so this CANNOT happen - if result == nil { - return nil, errors.New("nil result should not be possible") - } - return result, nil -} - -// selectPhantomImplV0 implements support for the legacy (buggy) client phantom -// address selection algorithm. -func selectPhantomImplV0(seed []byte, subnets []*phantomNet) (*PhantomIP, error) { - - addressTotal := big.NewInt(0) - - type idNet struct { - min, max big.Int - net *phantomNet - } - var idNets []idNet - - for _, _net := range subnets { - netMaskOnes, _ := _net.Mask.Size() - if ipv4net := _net.IP.To4(); ipv4net != nil { - _idNet := idNet{} - _idNet.min.Set(addressTotal) - addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(32-netMaskOnes)), nil)) - addressTotal.Sub(addressTotal, big.NewInt(1)) - _idNet.max.Set(addressTotal) - _idNet.net = _net - idNets = append(idNets, _idNet) - } else if ipv6net := _net.IP.To16(); ipv6net != nil { - _idNet := idNet{} - _idNet.min.Set(addressTotal) - addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(128-netMaskOnes)), nil)) - addressTotal.Sub(addressTotal, big.NewInt(1)) - _idNet.max.Set(addressTotal) - _idNet.net = _net - idNets = append(idNets, _idNet) - } else { - return nil, fmt.Errorf("failed to parse %v", _net) - } - } - - if addressTotal.Cmp(big.NewInt(0)) <= 0 { - return nil, ErrLegacyAddrSelectBug - } - - id := &big.Int{} - id.SetBytes(seed) - if id.Cmp(addressTotal) > 0 { - id.Mod(id, addressTotal) - } - - var result *PhantomIP - for _, _idNet := range idNets { - if _idNet.max.Cmp(id) >= 0 && _idNet.min.Cmp(id) == -1 { - res, err := SelectAddrFromSubnet(seed, _idNet.net.IPNet) - if err != nil { - return nil, fmt.Errorf("failed to chose IP address: %v", err) - } - result = &PhantomIP{IP: &res, SupportsPortRand: _idNet.net.supportRandomPort} - } - } - if result == nil { - return nil, ErrLegacyAddrSelectBug - } - return result, nil -} - -// SelectAddrFromSubnet - given a seed and a CIDR block choose an address. -// -// This is done by generating a seeded random bytes up to teh length of the full -// address then using the net mask to zero out any bytes that are already -// specified by the CIDR block. Tde masked random value is then added to the -// cidr block base giving the final randomly selected address. -func SelectAddrFromSubnet(seed []byte, net1 *net.IPNet) (net.IP, error) { - bits, addrLen := net1.Mask.Size() - - ipBigInt := &big.Int{} - if v4net := net1.IP.To4(); v4net != nil { - ipBigInt.SetBytes(net1.IP.To4()) - } else if v6net := net1.IP.To16(); v6net != nil { - ipBigInt.SetBytes(net1.IP.To16()) - } - - seedInt, n := binary.Varint(seed) - if n == 0 { - return nil, fmt.Errorf("failed to create seed ") - } - - // nolint:staticcheck // here for backwards compatibility with clients - mrand.Seed(seedInt) - randBytes := make([]byte, addrLen/8) - - // nolint:staticcheck // here for backwards compatibility with clients - _, err := mrand.Read(randBytes) - if err != nil { - return nil, err - } - randBigInt := &big.Int{} - randBigInt.SetBytes(randBytes) - - mask := make([]byte, addrLen/8) - for i := 0; i < addrLen/8; i++ { - mask[i] = 0xff - } - maskBigInt := &big.Int{} - maskBigInt.SetBytes(mask) - maskBigInt.Rsh(maskBigInt, uint(bits)) - - randBigInt.And(randBigInt, maskBigInt) - ipBigInt.Add(ipBigInt, randBigInt) - - return net.IP(ipBigInt.Bytes()), nil -} - -// SelectAddrFromSubnetOffset given a CIDR block and offset, return the net.IP -// -// Version 2: HKDF-based -func SelectAddrFromSubnetOffset(net1 *net.IPNet, offset *big.Int) (net.IP, error) { - bits, addrLen := net1.Mask.Size() - - // Compute network size (e.g. an ipv4 /24 is 2^(32-24) - var netSize big.Int - netSize.Exp(big.NewInt(2), big.NewInt(int64(addrLen-bits)), nil) - - // Check that offset is within this subnet - if netSize.Cmp(offset) <= 0 { - return nil, errors.New("Offset too big for subnet") - } - - ipBigInt := &big.Int{} - if v4net := net1.IP.To4(); v4net != nil { - ipBigInt.SetBytes(net1.IP.To4()) - } else if v6net := net1.IP.To16(); v6net != nil { - ipBigInt.SetBytes(net1.IP.To16()) - } - - ipBigInt.Add(ipBigInt, offset) - - return net.IP(ipBigInt.Bytes()), nil -} - -// selectPhantomImplHkdf selects an ip address from the list of subnets -// associated with the specified generation by constructing a set of start and -// end values for the high and low values in each allocation. The random number -// is then bound between the global min and max of that set. This ensures that -// addresses are chosen based on the number of addresses in the subnet. -func selectPhantomImplHkdf(seed []byte, subnets []*phantomNet) (*PhantomIP, error) { - type idNet struct { - min, max big.Int - net *phantomNet - } - var idNets []idNet - - // Compose a list of ID Nets with min, max and network associated and count - // the total number of available addresses. - addressTotal := big.NewInt(0) - for _, _net := range subnets { - netMaskOnes, _ := _net.Mask.Size() - if ipv4net := _net.IP.To4(); ipv4net != nil { - _idNet := idNet{} - _idNet.min.Set(addressTotal) - addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(32-netMaskOnes)), nil)) - _idNet.max.Sub(addressTotal, big.NewInt(1)) - _idNet.net = _net - idNets = append(idNets, _idNet) - } else if ipv6net := _net.IP.To16(); ipv6net != nil { - _idNet := idNet{} - _idNet.min.Set(addressTotal) - addressTotal.Add(addressTotal, big.NewInt(2).Exp(big.NewInt(2), big.NewInt(int64(128-netMaskOnes)), nil)) - _idNet.max.Sub(addressTotal, big.NewInt(1)) - _idNet.net = _net - idNets = append(idNets, _idNet) - } else { - return nil, fmt.Errorf("failed to parse %v", _net) - } - } - - // If the total number of addresses is 0 something has gone wrong - if addressTotal.Cmp(big.NewInt(0)) <= 0 { - return nil, ErrMissingAddrs - } - - // Pick a value using the seed in the range of between 0 and the total - // number of addresses. - hkdfReader := hkdf.New(sha256.New, seed, nil, []byte("phantom-addr-id")) - id, err := rand.Int(hkdfReader, addressTotal) - if err != nil { - return nil, err - } - - // Find the network (ID net) that contains our random value and select a - // random address from that subnet. - // min >= id%total >= max - var result *PhantomIP - for _, _idNet := range idNets { - // fmt.Printf("tot:%s, seed%%tot:%s id cmp max: %d, id cmp min: %d %s\n", addressTotal.String(), id, _idNet.max.Cmp(id), _idNet.min.Cmp(id), _idNet.net.String()) - if _idNet.max.Cmp(id) >= 0 && _idNet.min.Cmp(id) <= 0 { - - var offset big.Int - offset.Sub(id, &_idNet.min) - res, err := SelectAddrFromSubnetOffset(_idNet.net.IPNet, &offset) - if err != nil { - return nil, fmt.Errorf("failed to chose IP address: %v", err) - } - - result = &PhantomIP{IP: &res, SupportsPortRand: _idNet.net.supportRandomPort} - } - } - - // We want to make it so this CANNOT happen - if result == nil { - return nil, errors.New("nil result should not be possible") - } - return result, nil -} - -// GetSubnetsByGeneration - provide a generation index. If the generation exists -// the associated SubnetConfig is returned. If it is not defined the default -// subnets are returned. -func (p *PhantomIPSelector) GetSubnetsByGeneration(generation uint) *SubnetConfig { - if subnets, ok := p.Networks[generation]; ok { - return subnets - } - - // No Default subnets provided if the generation is not known - return nil -} - -// AddGeneration - add a subnet config as a new new generation, if the requested -// generation index is taken then it uses (and returns) the next available -// number. -func (p *PhantomIPSelector) AddGeneration(gen int, subnets *SubnetConfig) uint { - - ugen := uint(gen) - - if gen == -1 || p.IsTakenGeneration(ugen) { - ugen = p.newGenerationIndex() - } - - p.Networks[ugen] = subnets - return ugen -} - -func (p *PhantomIPSelector) newGenerationIndex() uint { - maxGen := uint(0) - for k := range p.Networks { - if k > maxGen { - maxGen = k - } - } - return maxGen + 1 -} - -// IsTakenGeneration - check if the generation index is already in use. -func (p *PhantomIPSelector) IsTakenGeneration(gen uint) bool { - if _, ok := p.Networks[gen]; ok { - return true - } - return false -} - -// RemoveGeneration - remove a generation from the mapping -func (p *PhantomIPSelector) RemoveGeneration(generation uint) bool { - p.Networks[generation] = nil - return true -} - -// UpdateGeneration - Update the subnet list associated with a specific generation -func (p *PhantomIPSelector) UpdateGeneration(generation uint, subnets *SubnetConfig) bool { - p.Networks[generation] = subnets - return true -} - -func init() { - // NOTE: math/rand is only used for backwards compatibility. - // nolint:staticcheck - mrand.Seed(time.Now().UnixNano()) -} diff --git a/pkg/station/lib/phantoms.go b/pkg/station/lib/phantoms.go deleted file mode 100644 index 201c30e6..00000000 --- a/pkg/station/lib/phantoms.go +++ /dev/null @@ -1,69 +0,0 @@ -package lib - -import ( - "fmt" - "os" - "strconv" - - toml "github.com/pelletier/go-toml" -) - -// ConjurePhantomSubnet - Weighted option to choose phantom address from. -type ConjurePhantomSubnet struct { - Weight uint32 - Subnets []string - RandomizeDstPort bool -} - -// SubnetConfig - Configuration of subnets for Conjure to choose a Phantom out of. -type SubnetConfig struct { - WeightedSubnets []ConjurePhantomSubnet -} - -// PhantomIPSelector - Object for tracking current generation to SubnetConfig Mapping. -type PhantomIPSelector struct { - Networks map[uint]*SubnetConfig -} - -// type shim because github.com/pelletier/go-toml doesn't allow for integer value keys to maps so -// we have to parse them ourselves. :( -type phantomIPSelectorInternal struct { - Networks map[string]*SubnetConfig -} - -// GetPhantomSubnetSelector gets the location of the configuration file from an -// environment variable and returns the parsed configuration. -func GetPhantomSubnetSelector() (*PhantomIPSelector, error) { - return SubnetsFromTomlFile(os.Getenv("PHANTOM_SUBNET_LOCATION")) -} - -// SubnetsFromTomlFile takes a path and parses the toml config file -func SubnetsFromTomlFile(path string) (*PhantomIPSelector, error) { - - tree, err := toml.LoadFile(path) - if err != nil { - return nil, fmt.Errorf("error opening configuration file: %v", err) - } - - var pss = &PhantomIPSelector{ - Networks: make(map[uint]*SubnetConfig), - } - // shim because github.com/pelletier/go-toml doesn't allow for integer value keys to maps so - // we have to parse them ourselves. :( - var phantomSelectorSet = &phantomIPSelectorInternal{} - err = tree.Unmarshal(phantomSelectorSet) - if err != nil { - return nil, fmt.Errorf("error unmarshalling configuration file: %v", err) - } - - for gen, set := range phantomSelectorSet.Networks { - g, err := strconv.Atoi(gen) - if err != nil { - return nil, err - } - // fmt.Printf("[GetPhantomSubnetSelector] adding %d, %+v\n", g, set) - pss.AddGeneration(g, set) - } - - return pss, nil -} diff --git a/pkg/station/lib/phantoms_test.go b/pkg/station/lib/phantoms_test.go deleted file mode 100644 index 9d7e25e8..00000000 --- a/pkg/station/lib/phantoms_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package lib - -import ( - "os" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestPhantomsParse(t *testing.T) { - os.Setenv("PHANTOM_SUBNET_LOCATION", "./test/phantom_subnets.toml") - conf, err := GetPhantomSubnetSelector() - require.Nil(t, err) - require.NotNil(t, conf) - - require.Equal(t, len(conf.Networks), 3) - - sc, ok := conf.Networks[957] - require.Equal(t, ok, true) - require.Equal(t, len(sc.WeightedSubnets), 2) - require.Equal(t, len(sc.WeightedSubnets[0].Subnets), 2) - require.True(t, sc.WeightedSubnets[0].RandomizeDstPort) - require.False(t, sc.WeightedSubnets[1].RandomizeDstPort) - require.Contains(t, sc.WeightedSubnets[0].Subnets, "192.122.190.0/24") -} diff --git a/pkg/station/lib/registration.go b/pkg/station/lib/registration.go index 5fbef6ba..79dd13eb 100644 --- a/pkg/station/lib/registration.go +++ b/pkg/station/lib/registration.go @@ -16,6 +16,7 @@ import ( "time" "github.com/refraction-networking/conjure/pkg/core" + "github.com/refraction-networking/conjure/pkg/phantoms" "github.com/refraction-networking/conjure/pkg/station/geoip" "github.com/refraction-networking/conjure/pkg/station/liveness" "github.com/refraction-networking/conjure/pkg/station/log" @@ -40,7 +41,7 @@ type RegistrationManager struct { registeredDecoys *RegisteredDecoys Logger *log.Logger - PhantomSelector *PhantomIPSelector + PhantomSelector *phantoms.PhantomIPSelector LivenessTester liveness.Tester GeoIP geoip.Database @@ -65,7 +66,7 @@ func NewRegistrationManager(conf *RegConfig) *RegistrationManager { logger.Fatal(err) } - p, err := NewPhantomIPSelector() + p, err := phantoms.NewPhantomIPSelector() if err != nil { logger.Errorf("failed to create the PhantomIPSelector object: %v", err) return nil @@ -99,7 +100,7 @@ func (regManager *RegistrationManager) OnReload(conf *RegConfig) { // try to re-initialize the phantom selector, if error occurs log err and // do not update the existing PhantomSelector - p, err := NewPhantomIPSelector() + p, err := phantoms.NewPhantomIPSelector() if err != nil { regManager.Logger.Errorf("failed to reload phantom subnets: %v", err) } else { diff --git a/pkg/station/lib/registration_ingest.go b/pkg/station/lib/registration_ingest.go index 87ffcbec..4d5e9e86 100644 --- a/pkg/station/lib/registration_ingest.go +++ b/pkg/station/lib/registration_ingest.go @@ -14,6 +14,7 @@ import ( "google.golang.org/protobuf/types/known/anypb" "github.com/refraction-networking/conjure/pkg/core" + "github.com/refraction-networking/conjure/pkg/phantoms" "github.com/refraction-networking/conjure/pkg/station/liveness" "github.com/refraction-networking/conjure/pkg/station/log" pb "github.com/refraction-networking/conjure/proto" @@ -100,7 +101,7 @@ func (rm *RegistrationManager) startIngestThread(ctx context.Context, regChan <- newRegs, err := rm.parseRegMessage(msg.([]byte)) if err != nil { - if !errors.Is(err, ErrLegacyAddrSelectBug) { + if !errors.Is(err, phantoms.ErrLegacyAddrSelectBug) { logger.Errorf("Encountered err when creating Reg: %v\n", err) } continue @@ -306,7 +307,7 @@ func (rm *RegistrationManager) parseRegMessage(msg []byte) ([]*DecoyRegistration reg, err := rm.NewRegistrationC2SWrapper(parsed, false) if err != nil { - if !errors.Is(err, ErrLegacyAddrSelectBug) { + if !errors.Is(err, phantoms.ErrLegacyAddrSelectBug) { logger.Errorf("Failed to create registration from v4 C2S: %v", err) } return nil, err @@ -360,7 +361,7 @@ func (rm *RegistrationManager) NewRegistration(c2s *pb.ClientToStation, conjureK return nil, fmt.Errorf("error handling transport params: %s", err) } - phantomPort, err := rm.getPhantomDstPort(c2s.GetTransport(), transportParams, conjureKeys.ConjureSeed, clientLibVer, phantomAddr.SupportsPortRand) + phantomPort, err := rm.getPhantomDstPort(c2s.GetTransport(), transportParams, conjureKeys.ConjureSeed, clientLibVer, phantomAddr.SupportRandomPort()) if err != nil { return nil, fmt.Errorf("error selecting phantom dst port: %s", err) } @@ -379,7 +380,7 @@ func (rm *RegistrationManager) NewRegistration(c2s *pb.ClientToStation, conjureK transportParams: transportParams, Flags: c2s.Flags, - PhantomIp: *phantomAddr.IP, + PhantomIp: *phantomAddr.IP(), PhantomPort: phantomPort, PhantomProto: phantomProto,