Compare commits
2 Commits
1411370b0e
...
1ea16d80e4
Author | SHA1 | Date | |
---|---|---|---|
1ea16d80e4 | |||
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,
|
||||
},
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
host, ok := currBootstrap.Hosts[hostName]
|
||||
if !ok {
|
||||
return nebula.Certificate{}, ErrHostNotFound
|
||||
}
|
||||
ip := host.IP()
|
||||
|
||||
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
|
||||
@ -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
|
||||
|
@ -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