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.
|
* It should end with a letter or number.
|
||||||
|
|
||||||
## Step 2: Choose IP
|
## Step 2: Create a `bootstrap.json` File
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
To create a `bootstrap.json` file for the new host, the admin should perform the
|
To create a `bootstrap.json` file for the new host, the admin should perform the
|
||||||
following command from their own host:
|
following command from their own host:
|
||||||
|
|
||||||
```
|
```
|
||||||
isle hosts create \
|
isle hosts create --hostname <name> >bootstrap.json
|
||||||
--hostname <name> \
|
|
||||||
--ip <ip> \
|
|
||||||
> bootstrap.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting `bootstrap.json` file should be treated as a secret file and
|
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",
|
"Name of the host to generate bootstrap.json for",
|
||||||
)
|
)
|
||||||
|
|
||||||
ipF := flags.VarPF(
|
flags.VarP(
|
||||||
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
|
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,8 +43,8 @@ var subCmdHostsCreate = subCmd{
|
|||||||
return fmt.Errorf("parsing flags: %w", err)
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hostNameF.Changed || !ipF.Changed {
|
if !hostNameF.Changed {
|
||||||
return errors.New("--hostname and --ip are required")
|
return errors.New("--hostname is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
var res daemon.CreateHostResult
|
var res daemon.CreateHostResult
|
||||||
@ -54,8 +54,8 @@ var subCmdHostsCreate = subCmd{
|
|||||||
"CreateHost",
|
"CreateHost",
|
||||||
daemon.CreateHostRequest{
|
daemon.CreateHostRequest{
|
||||||
HostName: hostName,
|
HostName: hostName,
|
||||||
IP: ip,
|
|
||||||
Opts: daemon.CreateHostOpts{
|
Opts: daemon.CreateHostOpts{
|
||||||
|
IP: ip,
|
||||||
CanCreateHosts: *canCreateHosts,
|
CanCreateHosts: *canCreateHosts,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
package daemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -23,6 +25,10 @@ import (
|
|||||||
|
|
||||||
// CreateHostOpts are optional parameters to the CreateHost method.
|
// CreateHostOpts are optional parameters to the CreateHost method.
|
||||||
type CreateHostOpts struct {
|
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
|
// CanCreateHosts indicates that the bootstrap produced by CreateHost should
|
||||||
// give the new host the ability to create new hosts as well.
|
// give the new host the ability to create new hosts as well.
|
||||||
CanCreateHosts bool
|
CanCreateHosts bool
|
||||||
@ -82,7 +88,6 @@ type Daemon interface {
|
|||||||
CreateHost(
|
CreateHost(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
hostName nebula.HostName,
|
hostName nebula.HostName,
|
||||||
ip netip.Addr, // TODO automatically choose IP address
|
|
||||||
opts CreateHostOpts,
|
opts CreateHostOpts,
|
||||||
) (
|
) (
|
||||||
JoiningBootstrap, error,
|
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(
|
func (d *daemon) CreateHost(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
hostName nebula.HostName,
|
hostName nebula.HostName,
|
||||||
ip netip.Addr,
|
|
||||||
opts CreateHostOpts,
|
opts CreateHostOpts,
|
||||||
) (
|
) (
|
||||||
JoiningBootstrap, error,
|
JoiningBootstrap, error,
|
||||||
@ -613,6 +696,16 @@ func (d *daemon) CreateHost(
|
|||||||
currBootstrap := d.currBootstrap
|
currBootstrap := d.currBootstrap
|
||||||
d.l.RUnlock()
|
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(
|
caSigningPrivateKey, err := getNebulaCASigningPrivateKey(
|
||||||
ctx, d.secretsStore,
|
ctx, d.secretsStore,
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"isle/bootstrap"
|
"isle/bootstrap"
|
||||||
"isle/nebula"
|
"isle/nebula"
|
||||||
"net/netip"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
@ -130,7 +129,6 @@ func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{},
|
|||||||
// All fields are required.
|
// All fields are required.
|
||||||
type CreateHostRequest struct {
|
type CreateHostRequest struct {
|
||||||
HostName nebula.HostName
|
HostName nebula.HostName
|
||||||
IP netip.Addr
|
|
||||||
Opts CreateHostOpts
|
Opts CreateHostOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +145,7 @@ func (r *RPC) CreateHost(
|
|||||||
CreateHostResult, error,
|
CreateHostResult, error,
|
||||||
) {
|
) {
|
||||||
joiningBootstrap, err := r.daemon.CreateHost(
|
joiningBootstrap, err := r.daemon.CreateHost(
|
||||||
ctx, req.HostName, req.IP, req.Opts,
|
ctx, req.HostName, req.Opts,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CreateHostResult{}, err
|
return CreateHostResult{}, err
|
||||||
|
@ -9,7 +9,7 @@ function do_tests {
|
|||||||
[ "$(echo "$hosts" | jq -r '.[0].Storage.Instances|length')" = "3" ]
|
[ "$(echo "$hosts" | jq -r '.[0].Storage.Instances|length')" = "3" ]
|
||||||
|
|
||||||
[ "$(echo "$hosts" | jq -r '.[1].Name')" = "secondus" ]
|
[ "$(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" ]
|
[ "$(echo "$hosts" | jq -r '.[1].Storage.Instances|length')" = "0" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ primus_base="$base/primus"
|
|||||||
primus_ip="10.6.9.1"
|
primus_ip="10.6.9.1"
|
||||||
|
|
||||||
secondus_base="$base/secondus"
|
secondus_base="$base/secondus"
|
||||||
secondus_ip="10.6.9.2"
|
|
||||||
|
|
||||||
function as_primus {
|
function as_primus {
|
||||||
current_ip="$primus_ip"
|
current_ip="$primus_ip"
|
||||||
@ -68,7 +67,6 @@ EOF
|
|||||||
echo "Creating secondus bootstrap"
|
echo "Creating secondus bootstrap"
|
||||||
isle hosts create \
|
isle hosts create \
|
||||||
--hostname secondus \
|
--hostname secondus \
|
||||||
--ip "$secondus_ip" \
|
|
||||||
> "$secondus_bootstrap"
|
> "$secondus_bootstrap"
|
||||||
|
|
||||||
(
|
(
|
||||||
@ -91,3 +89,11 @@ EOF
|
|||||||
isle network join -b "$secondus_bootstrap"
|
isle network join -b "$secondus_bootstrap"
|
||||||
)
|
)
|
||||||
fi
|
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