Compare commits

...

2 Commits

8 changed files with 117 additions and 57 deletions

View File

@ -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

View File

@ -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,
},
},

View File

@ -6,7 +6,6 @@ import (
"isle/daemon"
"isle/jsonutil"
"isle/nebula"
"net/netip"
"os"
)
@ -17,7 +16,6 @@ var subCmdNebulaCreateCert = subCmd{
var (
flags = subCmdCtx.flagSet(false)
hostName nebula.HostName
ip netip.Addr
)
hostNameF := flags.VarPF(
@ -31,12 +29,6 @@ var subCmdNebulaCreateCert = subCmd{
`Path to PEM file containing public key which will be embedded in the cert.`,
)
flags.Var(
textUnmarshalerFlag{&ip},
"ip",
"IP address to create a cert for. If this is not given then the IP associated with the host via its `hosts create` call will be used",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
@ -63,9 +55,6 @@ var subCmdNebulaCreateCert = subCmd{
daemon.CreateNebulaCertificateRequest{
HostName: hostName,
HostEncryptingPublicKey: hostPub,
Opts: daemon.CreateNebulaCertificateOpts{
IP: ip,
},
},
)
if err != nil {

View File

@ -92,6 +92,7 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
subCmdsMap := map[string]subCmd{}
for _, subCmd := range subCmds {
// TODO allow subCmd(s) in some cases
subCmdsMap[subCmd.name] = subCmd
}

View File

@ -3,7 +3,9 @@
package daemon
import (
"bytes"
"context"
"crypto/rand"
"errors"
"fmt"
"io"
@ -23,23 +25,15 @@ 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
}
// CreateNebulaCertificateOpts are optional parameters to the
// CreateNebulaCertificate method.
type CreateNebulaCertificateOpts struct {
// IP, if given will be used for the host's IP in the created cert. If this
// is given then it is not required that the host have an entry in garage.
//
// TODO once `hosts create` automatically adds the host to garage this can
// be removed.
IP netip.Addr
}
// Daemon presents all functionality required for client frontends to interact
// with isle, typically via the unix socket.
type Daemon interface {
@ -82,7 +76,6 @@ type Daemon interface {
CreateHost(
ctx context.Context,
hostName nebula.HostName,
ip netip.Addr, // TODO automatically choose IP address
opts CreateHostOpts,
) (
JoiningBootstrap, error,
@ -98,7 +91,6 @@ type Daemon interface {
ctx context.Context,
hostName nebula.HostName,
hostPubKey nebula.EncryptingPublicKey,
opts CreateNebulaCertificateOpts,
) (
nebula.Certificate, error,
)
@ -601,10 +593,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 +683,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,
)
@ -671,7 +751,6 @@ func (d *daemon) CreateNebulaCertificate(
ctx context.Context,
hostName nebula.HostName,
hostPubKey nebula.EncryptingPublicKey,
opts CreateNebulaCertificateOpts,
) (
nebula.Certificate, error,
) {
@ -680,14 +759,11 @@ func (d *daemon) CreateNebulaCertificate(
) (
nebula.Certificate, error,
) {
ip := opts.IP
if ip == (netip.Addr{}) {
host, ok := currBootstrap.Hosts[hostName]
if !ok {
return nebula.Certificate{}, ErrHostNotFound
}
ip = host.IP()
}
ip := host.IP()
caSigningPrivateKey, err := getNebulaCASigningPrivateKey(
ctx, d.secretsStore,

View File

@ -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
@ -163,7 +161,6 @@ func (r *RPC) CreateHost(
type CreateNebulaCertificateRequest struct {
HostName nebula.HostName
HostEncryptingPublicKey nebula.EncryptingPublicKey
Opts CreateNebulaCertificateOpts
}
// CreateNebulaCertificateResult wraps the results from the
@ -180,7 +177,7 @@ func (r *RPC) CreateNebulaCertificate(
CreateNebulaCertificateResult, error,
) {
cert, err := r.daemon.CreateNebulaCertificate(
ctx, req.HostName, req.HostEncryptingPublicKey, req.Opts,
ctx, req.HostName, req.HostEncryptingPublicKey,
)
if err != nil {
return CreateNebulaCertificateResult{}, err

View File

@ -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" ]
}

View File

@ -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
)"