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. * 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

View File

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

View File

@ -6,7 +6,6 @@ import (
"isle/daemon" "isle/daemon"
"isle/jsonutil" "isle/jsonutil"
"isle/nebula" "isle/nebula"
"net/netip"
"os" "os"
) )
@ -17,7 +16,6 @@ var subCmdNebulaCreateCert = subCmd{
var ( var (
flags = subCmdCtx.flagSet(false) flags = subCmdCtx.flagSet(false)
hostName nebula.HostName hostName nebula.HostName
ip netip.Addr
) )
hostNameF := flags.VarPF( hostNameF := flags.VarPF(
@ -31,12 +29,6 @@ var subCmdNebulaCreateCert = subCmd{
`Path to PEM file containing public key which will be embedded in the cert.`, `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 { if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -63,9 +55,6 @@ var subCmdNebulaCreateCert = subCmd{
daemon.CreateNebulaCertificateRequest{ daemon.CreateNebulaCertificateRequest{
HostName: hostName, HostName: hostName,
HostEncryptingPublicKey: hostPub, HostEncryptingPublicKey: hostPub,
Opts: daemon.CreateNebulaCertificateOpts{
IP: ip,
},
}, },
) )
if err != nil { if err != nil {

View File

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

View File

@ -3,7 +3,9 @@
package daemon package daemon
import ( import (
"bytes"
"context" "context"
"crypto/rand"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -23,23 +25,15 @@ 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
} }
// 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 // Daemon presents all functionality required for client frontends to interact
// with isle, typically via the unix socket. // with isle, typically via the unix socket.
type Daemon interface { type Daemon interface {
@ -82,7 +76,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,
@ -98,7 +91,6 @@ type Daemon interface {
ctx context.Context, ctx context.Context,
hostName nebula.HostName, hostName nebula.HostName,
hostPubKey nebula.EncryptingPublicKey, hostPubKey nebula.EncryptingPublicKey,
opts CreateNebulaCertificateOpts,
) ( ) (
nebula.Certificate, error, 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( 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 +683,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,
) )
@ -671,7 +751,6 @@ func (d *daemon) CreateNebulaCertificate(
ctx context.Context, ctx context.Context,
hostName nebula.HostName, hostName nebula.HostName,
hostPubKey nebula.EncryptingPublicKey, hostPubKey nebula.EncryptingPublicKey,
opts CreateNebulaCertificateOpts,
) ( ) (
nebula.Certificate, error, nebula.Certificate, error,
) { ) {
@ -680,14 +759,11 @@ func (d *daemon) CreateNebulaCertificate(
) ( ) (
nebula.Certificate, error, nebula.Certificate, error,
) { ) {
ip := opts.IP host, ok := currBootstrap.Hosts[hostName]
if ip == (netip.Addr{}) { if !ok {
host, ok := currBootstrap.Hosts[hostName] return nebula.Certificate{}, ErrHostNotFound
if !ok {
return nebula.Certificate{}, ErrHostNotFound
}
ip = host.IP()
} }
ip := host.IP()
caSigningPrivateKey, err := getNebulaCASigningPrivateKey( caSigningPrivateKey, err := getNebulaCASigningPrivateKey(
ctx, d.secretsStore, ctx, d.secretsStore,

View File

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

View File

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