Skip to content

Commit

Permalink
Phantom selection centralization (#241)
Browse files Browse the repository at this point in the history
centralizing and fleshing out tests for phantom selections including backward compatibility
  • Loading branch information
jmwample authored Oct 7, 2023
1 parent b8ea79d commit 3805036
Show file tree
Hide file tree
Showing 20 changed files with 1,612 additions and 1,059 deletions.
23 changes: 23 additions & 0 deletions internal/compatability/README.md
Original file line number Diff line number Diff line change
@@ -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
277 changes: 277 additions & 0 deletions internal/compatability/v0/compat.go
Original file line number Diff line number Diff line change
@@ -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"},
},
},
}
}
Loading

0 comments on commit 3805036

Please sign in to comment.