Automatically choose IP for new hosts
This commit is contained in:
parent
1411370b0e
commit
ee30199c4c
@ -15,22 +15,13 @@ conform to the following rules:
|
||||
|
||||
* It should end with a letter or number.
|
||||
|
||||
## Step 2: Choose IP
|
||||
|
||||
The admin should choose an IP for the host. The IP you choose for the new host
|
||||
should be one which is not yet used by any other host and is in a subnet which
|
||||
was configured when creating the network.
|
||||
|
||||
## Step 3: Create a `bootstrap.json` File
|
||||
## Step 2: Create a `bootstrap.json` File
|
||||
|
||||
To create a `bootstrap.json` file for the new host, the admin should perform the
|
||||
following command from their own host:
|
||||
|
||||
```
|
||||
isle hosts create \
|
||||
--hostname <name> \
|
||||
--ip <ip> \
|
||||
> bootstrap.json
|
||||
isle hosts create --hostname <name> >bootstrap.json
|
||||
```
|
||||
|
||||
The resulting `bootstrap.json` file should be treated as a secret file and
|
||||
|
@ -29,7 +29,7 @@ var subCmdHostsCreate = subCmd{
|
||||
"Name of the host to generate bootstrap.json for",
|
||||
)
|
||||
|
||||
ipF := flags.VarPF(
|
||||
flags.VarP(
|
||||
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
|
||||
)
|
||||
|
||||
@ -43,8 +43,8 @@ var subCmdHostsCreate = subCmd{
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if !hostNameF.Changed || !ipF.Changed {
|
||||
return errors.New("--hostname and --ip are required")
|
||||
if !hostNameF.Changed {
|
||||
return errors.New("--hostname is required")
|
||||
}
|
||||
|
||||
var res daemon.CreateHostResult
|
||||
@ -54,8 +54,8 @@ var subCmdHostsCreate = subCmd{
|
||||
"CreateHost",
|
||||
daemon.CreateHostRequest{
|
||||
HostName: hostName,
|
||||
IP: ip,
|
||||
Opts: daemon.CreateHostOpts{
|
||||
IP: ip,
|
||||
CanCreateHosts: *canCreateHosts,
|
||||
},
|
||||
},
|
||||
|
@ -3,7 +3,9 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -23,6 +25,10 @@ import (
|
||||
|
||||
// CreateHostOpts are optional parameters to the CreateHost method.
|
||||
type CreateHostOpts struct {
|
||||
// IP address of the new host. An IP address will be randomly chosen if one
|
||||
// is not given here.
|
||||
IP netip.Addr
|
||||
|
||||
// CanCreateHosts indicates that the bootstrap produced by CreateHost should
|
||||
// give the new host the ability to create new hosts as well.
|
||||
CanCreateHosts bool
|
||||
@ -82,7 +88,6 @@ type Daemon interface {
|
||||
CreateHost(
|
||||
ctx context.Context,
|
||||
hostName nebula.HostName,
|
||||
ip netip.Addr, // TODO automatically choose IP address
|
||||
opts CreateHostOpts,
|
||||
) (
|
||||
JoiningBootstrap, error,
|
||||
@ -601,10 +606,88 @@ func makeCACreds(
|
||||
}
|
||||
}
|
||||
|
||||
func chooseAvailableIP(b bootstrap.Bootstrap) (netip.Addr, error) {
|
||||
var (
|
||||
cidrIPNet = b.CAPublicCredentials.Cert.Unwrap().Details.Subnets[0]
|
||||
cidrMask = cidrIPNet.Mask
|
||||
cidrIPB = cidrIPNet.IP
|
||||
|
||||
cidr = netip.MustParsePrefix(cidrIPNet.String())
|
||||
cidrIP = cidr.Addr()
|
||||
cidrSuffixBits = cidrIP.BitLen() - cidr.Bits()
|
||||
|
||||
inUseIPs = make(map[netip.Addr]struct{}, len(b.Hosts))
|
||||
)
|
||||
|
||||
for _, host := range b.Hosts {
|
||||
inUseIPs[host.IP()] = struct{}{}
|
||||
}
|
||||
|
||||
// first check that there are any addresses at all. We can determine the
|
||||
// number of possible addresses using the network CIDR. The first IP in a
|
||||
// subnet is the network identifier, and is reserved. The last IP is the
|
||||
// broadcast IP, and is also reserved. Hence, the -2.
|
||||
usableIPs := (1 << cidrSuffixBits) - 2
|
||||
if len(inUseIPs) >= usableIPs {
|
||||
return netip.Addr{}, errors.New("no available IPs")
|
||||
}
|
||||
|
||||
// We need to know the subnet broadcast address, so we don't accidentally
|
||||
// produce it.
|
||||
cidrBCastIPB := bytes.Clone(cidrIPB)
|
||||
for i := range cidrBCastIPB {
|
||||
cidrBCastIPB[i] |= ^cidrMask[i]
|
||||
}
|
||||
cidrBCastIP, ok := netip.AddrFromSlice(cidrBCastIPB)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("invalid broadcast ip calculated: %x", cidrBCastIP))
|
||||
}
|
||||
|
||||
// Try a handful of times to pick an IP at random. This is preferred, as it
|
||||
// leaves less room for two different CreateHost calls to choose the same
|
||||
// IP.
|
||||
for range 20 {
|
||||
b := make([]byte, len(cidrIPB))
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading random bytes: %w", err)
|
||||
}
|
||||
|
||||
for i := range b {
|
||||
b[i] = cidrIPB[i] | (b[i] & ^cidrMask[i])
|
||||
}
|
||||
|
||||
ip, ok := netip.AddrFromSlice(b)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("generated invalid IP: %x", b))
|
||||
} else if !cidr.Contains(ip) {
|
||||
panic(fmt.Sprintf(
|
||||
"generated IP %v which is not in cidr %v", ip, cidr,
|
||||
))
|
||||
}
|
||||
|
||||
if ip == cidrIP || ip == cidrBCastIP {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, inUse := inUseIPs[ip]; !inUse {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If randomly picking fails then just go through IPs one by one until the
|
||||
// free one is found.
|
||||
for ip := cidrIP.Next(); ip != cidrBCastIP; ip = ip.Next() {
|
||||
if _, inUse := inUseIPs[ip]; !inUse {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
panic("All ips are in-use, but somehow that wasn't determined earlier")
|
||||
}
|
||||
|
||||
func (d *daemon) CreateHost(
|
||||
ctx context.Context,
|
||||
hostName nebula.HostName,
|
||||
ip netip.Addr,
|
||||
opts CreateHostOpts,
|
||||
) (
|
||||
JoiningBootstrap, error,
|
||||
@ -613,6 +696,16 @@ func (d *daemon) CreateHost(
|
||||
currBootstrap := d.currBootstrap
|
||||
d.l.RUnlock()
|
||||
|
||||
ip := opts.IP
|
||||
if ip == (netip.Addr{}) {
|
||||
var err error
|
||||
if ip, err = chooseAvailableIP(currBootstrap); err != nil {
|
||||
return JoiningBootstrap{}, fmt.Errorf(
|
||||
"choosing available IP: %w", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
caSigningPrivateKey, err := getNebulaCASigningPrivateKey(
|
||||
ctx, d.secretsStore,
|
||||
)
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"isle/bootstrap"
|
||||
"isle/nebula"
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
@ -130,7 +129,6 @@ func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{},
|
||||
// All fields are required.
|
||||
type CreateHostRequest struct {
|
||||
HostName nebula.HostName
|
||||
IP netip.Addr
|
||||
Opts CreateHostOpts
|
||||
}
|
||||
|
||||
@ -147,7 +145,7 @@ func (r *RPC) CreateHost(
|
||||
CreateHostResult, error,
|
||||
) {
|
||||
joiningBootstrap, err := r.daemon.CreateHost(
|
||||
ctx, req.HostName, req.IP, req.Opts,
|
||||
ctx, req.HostName, req.Opts,
|
||||
)
|
||||
if err != nil {
|
||||
return CreateHostResult{}, err
|
||||
|
@ -9,7 +9,7 @@ function do_tests {
|
||||
[ "$(echo "$hosts" | jq -r '.[0].Storage.Instances|length')" = "3" ]
|
||||
|
||||
[ "$(echo "$hosts" | jq -r '.[1].Name')" = "secondus" ]
|
||||
[ "$(echo "$hosts" | jq -r '.[1].VPN.IP')" = "10.6.9.2" ]
|
||||
[ "$(echo "$hosts" | jq -r '.[1].VPN.IP')" = "$secondus_ip" ]
|
||||
[ "$(echo "$hosts" | jq -r '.[1].Storage.Instances|length')" = "0" ]
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@ primus_base="$base/primus"
|
||||
primus_ip="10.6.9.1"
|
||||
|
||||
secondus_base="$base/secondus"
|
||||
secondus_ip="10.6.9.2"
|
||||
|
||||
function as_primus {
|
||||
current_ip="$primus_ip"
|
||||
@ -68,7 +67,6 @@ EOF
|
||||
echo "Creating secondus bootstrap"
|
||||
isle hosts create \
|
||||
--hostname secondus \
|
||||
--ip "$secondus_ip" \
|
||||
> "$secondus_bootstrap"
|
||||
|
||||
(
|
||||
@ -91,3 +89,11 @@ EOF
|
||||
isle network join -b "$secondus_bootstrap"
|
||||
)
|
||||
fi
|
||||
|
||||
secondus_ip="$(
|
||||
nebula-cert print -json \
|
||||
-path <(jq -r '.Bootstrap.Hosts["secondus"].PublicCredentials.Cert' "$secondus_bootstrap") \
|
||||
| jq -r '.details.ips[0]' \
|
||||
| cut -d/ -f1
|
||||
)"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user