Compare commits

..

No commits in common. "736b23429c02d5fd660994883d13b68658d9915a" and "279c79a9f18cb4affe783185a76e1e21fbef5919" have entirely different histories.

20 changed files with 153 additions and 218 deletions

View File

@ -103,7 +103,7 @@ To create the network, and the `admin.json` file in the process, run:
```
sudo isle network create \
--name <name> \
--ip-net <subnet> \
--ip-net <ip/subnet-prefix> \
--domain <domain> \
--hostname <hostname> \
| gpg -e -r <my gpg email> \
@ -112,6 +112,11 @@ sudo isle network create \
A couple of notes here:
* The `--ip-net` parameter is formed from both the subnet and the IP you chose
within it. So if your subnet is `10.10.0.0/16`, and your chosen IP in that
subnet is `10.10.4.20`, then your `--ip-net` parameter will be
`10.10.4.20/16`.
* Only one gpg recipient is specified. If you intend on including other users as
network administrators you can add them to the recipients list at this step,
so they will be able to use the `admin.json` file as well. You can also

View File

@ -10,7 +10,7 @@ import (
"isle/admin"
"isle/garage"
"isle/nebula"
"net/netip"
"net"
"os"
"path/filepath"
"sort"
@ -51,7 +51,7 @@ type Bootstrap struct {
HostAssigned `json:"-"`
SignedHostAssigned nebula.Signed[HostAssigned] // signed by CA
Hosts map[nebula.HostName]Host
Hosts map[string]Host
}
// New initializes and returns a new Bootstrap file for a new host. This
@ -60,8 +60,8 @@ func New(
caCreds nebula.CACredentials,
adminCreationParams admin.CreationParams,
garage Garage,
name nebula.HostName,
ip netip.Addr,
name string,
ip net.IP,
) (
Bootstrap, error,
) {
@ -89,7 +89,7 @@ func New(
PrivateCredentials: hostPrivCreds,
HostAssigned: assigned,
SignedHostAssigned: signedAssigned,
Hosts: map[nebula.HostName]Host{},
Hosts: map[string]Host{},
}, nil
}
@ -145,7 +145,7 @@ func (b Bootstrap) ThisHost() Host {
}
// Hash returns a deterministic hash of the given hosts map.
func HostsHash(hostsMap map[nebula.HostName]Host) ([]byte, error) {
func HostsHash(hostsMap map[string]Host) ([]byte, error) {
hosts := make([]Host, 0, len(hostsMap))
for _, host := range hostsMap {

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"isle/garage"
"isle/nebula"
"path/filepath"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
@ -47,12 +46,12 @@ func (b Bootstrap) GetGarageBootstrapHosts(
ctx context.Context,
logger *mlog.Logger,
) (
map[nebula.HostName]Host, error,
map[string]Host, error,
) {
client := b.GlobalBucketS3APIClient()
hosts := map[nebula.HostName]Host{}
hosts := map[string]Host{}
objInfoCh := client.ListObjects(
ctx, garage.GlobalBucket,

View File

@ -28,7 +28,7 @@ type GarageHost struct {
// HostAssigned are all fields related to a host which were assigned to it by an
// admin.
type HostAssigned struct {
Name nebula.HostName
Name string
PublicCredentials nebula.HostPublicCredentials
}

View File

@ -8,7 +8,7 @@ import (
"isle/admin"
"isle/bootstrap"
"isle/nebula"
"net/netip"
"net"
"os"
)
@ -46,20 +46,17 @@ var subCmdAdminCreateBootstrap = subCmd{
descr: "Creates a new bootstrap.json file for a particular host and writes it to stdout",
checkLock: false,
do: func(subCmdCtx subCmdCtx) error {
var (
flags = subCmdCtx.flagSet(false)
hostName nebula.HostName
ip netip.Addr
)
hostNameF := flags.VarPF(
textUnmarshalerFlag{&hostName},
"hostname", "h",
flags := subCmdCtx.flagSet(false)
hostName := flags.StringP(
"hostname", "h", "",
"Name of the host to generate bootstrap.json for",
)
ipF := flags.VarPF(
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
ipStr := flags.StringP(
"ip", "i", "",
"IP of the new host",
)
adminPath := flags.StringP(
@ -71,12 +68,20 @@ var subCmdAdminCreateBootstrap = subCmd{
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed ||
!ipF.Changed ||
*adminPath == "" {
if *hostName == "" || *ipStr == "" || *adminPath == "" {
return errors.New("--hostname, --ip, and --admin-path are required")
}
if err := validateHostName(*hostName); err != nil {
return fmt.Errorf("invalid hostname %q: %w", *hostName, err)
}
ip := net.ParseIP(*ipStr)
if ip == nil {
return fmt.Errorf("invalid ip %q", *ipStr)
}
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
@ -92,21 +97,19 @@ var subCmdAdminCreateBootstrap = subCmd{
adm.Nebula.CACredentials,
adm.CreationParams,
garageBootstrap,
hostName,
*hostName,
ip,
)
if err != nil {
return fmt.Errorf("initializing bootstrap data: %w", err)
}
hostsRes, err := subCmdCtx.getHosts()
hostBootstrap, err := loadHostBootstrap()
if err != nil {
return fmt.Errorf("getting hosts: %w", err)
return fmt.Errorf("loading host bootstrap: %w", err)
}
for _, host := range hostsRes.Hosts {
newHostBootstrap.Hosts[host.Name] = host
}
newHostBootstrap.Hosts = hostBootstrap.Hosts
return newHostBootstrap.WriteTo(os.Stdout)
},
@ -117,20 +120,17 @@ var subCmdAdminCreateNebulaCert = subCmd{
descr: "Creates a signed nebula certificate file and writes it to stdout",
checkLock: false,
do: func(subCmdCtx subCmdCtx) error {
var (
flags = subCmdCtx.flagSet(false)
hostName nebula.HostName
ip netip.Addr
flags := subCmdCtx.flagSet(false)
hostName := flags.StringP(
"hostname", "h", "",
"Name of the host to generate bootstrap.json for",
)
hostNameF := flags.VarPF(
textUnmarshalerFlag{&hostName},
"hostname", "h",
"Name of the host to generate a certificate for",
)
ipF := flags.VarPF(
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
ipStr := flags.StringP(
"ip", "i", "",
"IP of the new host",
)
adminPath := flags.StringP(
@ -147,13 +147,20 @@ var subCmdAdminCreateNebulaCert = subCmd{
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed ||
!ipF.Changed ||
*adminPath == "" ||
*pubKeyPath == "" {
if *hostName == "" || *ipStr == "" || *adminPath == "" || *pubKeyPath == "" {
return errors.New("--hostname, --ip, --admin-path, and --pub-key-path are required")
}
if err := validateHostName(*hostName); err != nil {
return fmt.Errorf("invalid hostname %q: %w", *hostName, err)
}
ip := net.ParseIP(*ipStr)
if ip == nil {
return fmt.Errorf("invalid ip %q", *ipStr)
}
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.json with --admin-path of %q: %w", *adminPath, err)
@ -170,7 +177,7 @@ var subCmdAdminCreateNebulaCert = subCmd{
}
nebulaHostCert, err := nebula.NewHostCert(
adm.Nebula.CACredentials, hostPub, hostName, ip,
adm.Nebula.CACredentials, hostPub, *hostName, ip,
)
if err != nil {
return fmt.Errorf("creating cert: %w", err)

View File

@ -1,15 +0,0 @@
package main
import (
"fmt"
"isle/daemon"
)
func (ctx subCmdCtx) getHosts() (daemon.GetHostsResult, error) {
var res daemon.GetHostsResult
err := ctx.daemonRCPClient.Call(ctx.ctx, &res, "GetHosts", nil)
if err != nil {
return daemon.GetHostsResult{}, fmt.Errorf("calling GetHosts: %w", err)
}
return res, nil
}

View File

@ -1,22 +0,0 @@
package main
import (
"encoding"
"fmt"
)
type textUnmarshalerFlag struct {
inner interface {
encoding.TextUnmarshaler
}
}
func (f textUnmarshalerFlag) Set(v string) error {
return f.inner.UnmarshalText([]byte(v))
}
func (f textUnmarshalerFlag) String() string {
return fmt.Sprint(f.inner)
}
func (f textUnmarshalerFlag) Type() string { return "string" }

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"isle/bootstrap"
"isle/daemon"
"isle/jsonutil"
"os"
"regexp"
@ -25,7 +26,11 @@ var subCmdHostsList = subCmd{
name: "list",
descr: "Lists all hosts in the network, and their IPs",
do: func(subCmdCtx subCmdCtx) error {
hostsRes, err := subCmdCtx.getHosts()
ctx := subCmdCtx.ctx
var res daemon.GetHostsResult
err := subCmdCtx.daemonRCPClient.Call(ctx, &res, "GetHosts", nil)
if err != nil {
return fmt.Errorf("calling GetHosts: %w", err)
}
@ -38,11 +43,11 @@ var subCmdHostsList = subCmd{
Storage bootstrap.GarageHost `json:",omitempty"`
}
hosts := make([]host, 0, len(hostsRes.Hosts))
for _, h := range hostsRes.Hosts {
hosts := make([]host, 0, len(res.Hosts))
for _, h := range res.Hosts {
host := host{
Name: string(h.Name),
Name: h.Name,
Storage: h.Garage,
}

View File

@ -29,16 +29,16 @@ var subCmdNetworkCreate = subCmd{
"Domain name that should be used as the root domain in the network.",
)
ipNetF := flags.VarPF(
textUnmarshalerFlag{&req.IPNet}, "ip-net", "i",
`An IP subnet, in CIDR form, which will be the overall range of`+
` possible IPs in the network. The first IP in this network`+
` range will become this first host's IP.`,
flags.StringVarP(
&req.IPNet, "ip-net", "i", "",
`IP+prefix (e.g. "10.10.0.1/16") which denotes the IP of this`+
` host, which will be the first host in the network, and the`+
` range of IPs which other hosts in the network can be`+
` assigned`,
)
hostNameF := flags.VarPF(
textUnmarshalerFlag{&req.HostName},
"hostname", "h",
flags.StringVarP(
&req.HostName, "hostname", "h", "",
"Name of this host, which will be the first host in the network",
)
@ -48,8 +48,8 @@ var subCmdNetworkCreate = subCmd{
if req.Name == "" ||
req.Domain == "" ||
!ipNetF.Changed ||
!hostNameF.Changed {
req.IPNet == "" ||
req.HostName == "" {
return errors.New("--name, --domain, --ip-net, and --hostname are required")
}

View File

@ -22,7 +22,7 @@ func dnsmasqPmuxProcConfig(
hostsSlice := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts))
for _, host := range hostBootstrap.Hosts {
hostsSlice = append(hostsSlice, dnsmasq.ConfDataHost{
Name: string(host.Name),
Name: host.Name,
IP: host.IP().String(),
})
}

View File

@ -197,7 +197,7 @@ func GarageApplyLayout(
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
zone := string(hostName)
zone := hostName
if alloc.Zone != "" {
zone = alloc.Zone
}

View File

@ -11,15 +11,29 @@ import (
"io/fs"
"isle/admin"
"isle/bootstrap"
"isle/daemon/jsonrpc2"
"isle/garage"
"isle/nebula"
"net"
"os"
"regexp"
"sync"
"time"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
var hostNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
func validateHostName(name string) error {
if !hostNameRegexp.MatchString(name) {
return errors.New("a host's name must start with a letter and only contain letters, numbers, and dashes")
}
return nil
}
// Daemon presents all functionality required for client frontends to interact
// with isle, typically via the unix socket.
type Daemon interface {
@ -27,17 +41,15 @@ type Daemon interface {
// CreateNetwork will initialize a new network using the given parameters.
// - name: Human-readable name of the network.
// - domain: Primary domain name that network services are served under.
// - ipNet: An IP subnet, in CIDR form, which will be the overall range of
// possible IPs in the network. The first IP in this network range will
// become this first host's IP.
// - ipNetStr: An IP + subnet mask which represents both the IP of this
// first host in the network, as well as the overall range of possible IPs
// in the network.
// - hostName: The name of this first host in the network.
//
// An Admin instance is returned, which is necessary to perform admin
// actions in the future.
CreateNetwork(
ctx context.Context, name, domain string,
ipNet nebula.IPNet,
hostName nebula.HostName,
ctx context.Context, name, domain, ipNetStr, hostName string,
) (
admin.Admin, error,
)
@ -49,11 +61,12 @@ type Daemon interface {
// - ErrAlreadyJoined
JoinNetwork(context.Context, bootstrap.Bootstrap) error
// GetBootstrapHosts returns the hosts stored in the bootstrap.
GetBootstrapHosts(
// GetGarageBootstrapHosts loads (and verifies) the <hostname>.json.signed
// file for all hosts stored in garage.
GetGarageBootstrapHosts(
ctx context.Context,
) (
map[nebula.HostName]bootstrap.Host, error,
map[string]bootstrap.Host, error,
)
// Shutdown blocks until all resources held or created by the daemon,
@ -136,7 +149,10 @@ type daemon struct {
// it should restart itself only when there's something actually requiring a
// restart.
func NewDaemon(
logger *mlog.Logger, daemonConfig Config, envBinDirPath string, opts *Opts,
logger *mlog.Logger,
daemonConfig Config,
envBinDirPath string,
opts *Opts,
) (
Daemon, error,
) {
@ -463,11 +479,24 @@ func (d *daemon) restartLoop(ctx context.Context, readyCh chan<- struct{}) {
func (d *daemon) CreateNetwork(
ctx context.Context,
name, domain string, ipNet nebula.IPNet, hostName nebula.HostName,
name, domain, ipNetStr, hostName string,
) (
admin.Admin, error,
) {
nebulaCACreds, err := nebula.NewCACredentials(domain, ipNet)
ip, subnet, err := net.ParseCIDR(ipNetStr)
if err != nil {
return admin.Admin{}, jsonrpc2.NewInvalidParamsError(
"parsing %q as a CIDR: %v", ipNetStr, err,
)
}
if err := validateHostName(hostName); err != nil {
return admin.Admin{}, jsonrpc2.NewInvalidParamsError(
"invalid hostname: %v", err,
)
}
nebulaCACreds, err := nebula.NewCACredentials(domain, subnet)
if err != nil {
return admin.Admin{}, fmt.Errorf("creating nebula CA cert: %w", err)
}
@ -490,7 +519,7 @@ func (d *daemon) CreateNetwork(
adm.CreationParams,
garageBootstrap,
hostName,
ipNet.FirstAddr(),
ip,
)
if err != nil {
return adm, fmt.Errorf("initializing bootstrap data: %w", err)
@ -565,17 +594,17 @@ func (d *daemon) JoinNetwork(
}
}
func (d *daemon) GetBootstrapHosts(
func (d *daemon) GetGarageBootstrapHosts(
ctx context.Context,
) (
map[nebula.HostName]bootstrap.Host, error,
map[string]bootstrap.Host, error,
) {
return withCurrBootstrap(d, func(
currBootstrap bootstrap.Bootstrap,
) (
map[nebula.HostName]bootstrap.Host, error,
map[string]bootstrap.Host, error,
) {
return currBootstrap.Hosts, nil
return getGarageBootstrapHosts(ctx, d.logger, currBootstrap)
})
}

View File

@ -86,7 +86,7 @@ func (d *daemon) putGarageBoostrapHost(ctx context.Context) error {
filePath := filepath.Join(
garageGlobalBucketBootstrapHostsDirPath,
string(host.Name)+".json.signed",
host.Name+".json.signed",
)
_, err = client.PutObject(
@ -108,12 +108,12 @@ func (d *daemon) putGarageBoostrapHost(ctx context.Context) error {
func getGarageBootstrapHosts(
ctx context.Context, logger *mlog.Logger, currBootstrap bootstrap.Bootstrap,
) (
map[nebula.HostName]bootstrap.Host, error,
map[string]bootstrap.Host, error,
) {
var (
b = currBootstrap
client = b.GlobalBucketS3APIClient()
hosts = map[nebula.HostName]bootstrap.Host{}
hosts = map[string]bootstrap.Host{}
objInfoCh = client.ListObjects(
ctx, garage.GlobalBucket,

View File

@ -6,7 +6,6 @@ import (
"fmt"
"isle/admin"
"isle/bootstrap"
"isle/nebula"
"slices"
"golang.org/x/exp/maps"
@ -33,13 +32,12 @@ type CreateNetworkRequest struct {
// Primary domain name that network services are served under.
Domain string
// An IP subnet, in CIDR form, which will be the overall range of possible
// IPs in the network. The first IP in this network range will become this
// first host's IP.
IPNet nebula.IPNet
// An IP + subnet mask which represents both the IP of this first host in
// the network, as well as the overall range of possible IPs in the network.
IPNet string
// The name of this first host in the network.
HostName nebula.HostName
HostName string
}
// CreateNetwork passes through to the Daemon method of the same name.
@ -73,7 +71,7 @@ func (r *RPC) GetHosts(
) (
GetHostsResult, error,
) {
hostsMap, err := r.daemon.GetBootstrapHosts(ctx)
hostsMap, err := r.daemon.GetGarageBootstrapHosts(ctx)
if err != nil {
return GetHostsResult{}, fmt.Errorf("retrieving hosts: %w", err)
}

View File

@ -1,22 +0,0 @@
package nebula
import (
"errors"
"regexp"
)
var hostNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
// HostName is the name of a host in a network. HostNames may only have
// lowercase letters, numbers, and hyphens, and must start with a letter.
type HostName string
// UnmarshalText parses and validates a HostName from a text string.
func (h *HostName) UnmarshalText(b []byte) error {
if !hostNameRegexp.Match(b) {
return errors.New("a host's name must start with a letter and only contain letters, numbers, and dashes")
}
*h = HostName(b)
return nil
}

View File

@ -1,43 +0,0 @@
package nebula
import (
"fmt"
"net"
"net/netip"
)
// IPNet is the CIDR of a nebula network.
type IPNet net.IPNet
// UnmarshalText parses and validates an IPNet from a text string.
func (n *IPNet) UnmarshalText(b []byte) error {
str := string(b)
_, subnet, err := net.ParseCIDR(str)
if err != nil {
return err
}
if cstr := subnet.String(); cstr != str {
return fmt.Errorf("IPNet is not given in its canonical form of %q", cstr)
}
if !subnet.IP.IsPrivate() {
return fmt.Errorf("IPNet is not in a private IP range")
}
*n = IPNet(*subnet)
return nil
}
func (n IPNet) String() string {
return (*net.IPNet)(&n).String()
}
func (n IPNet) MarshalText() ([]byte, error) {
return []byte(n.String()), nil
}
// FirstAddr returns the first IP address in the subnet.
func (n IPNet) FirstAddr() netip.Addr {
return netip.MustParseAddr(n.IP.String()).Next()
}

View File

@ -5,7 +5,6 @@ package nebula
import (
"fmt"
"net"
"net/netip"
"time"
"github.com/slackhq/nebula/cert"
@ -45,15 +44,12 @@ type CACredentials struct {
func NewHostCert(
caCreds CACredentials,
hostPub EncryptingPublicKey,
hostName HostName,
ip netip.Addr,
hostName string,
ip net.IP,
) (
Certificate, error,
) {
var (
caCert = caCreds.Public.Cert
ipSlice = net.IP(ip.AsSlice())
)
caCert := caCreds.Public.Cert
issuer, err := caCert.inner.Sha256Sum()
if err != nil {
@ -63,15 +59,15 @@ func NewHostCert(
expireAt := caCert.inner.Details.NotAfter.Add(-1 * time.Second)
subnet := caCert.inner.Details.Subnets[0]
if !subnet.Contains(ipSlice) {
if !subnet.Contains(ip) {
return Certificate{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
}
hostCert := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: string(hostName),
Name: hostName,
Ips: []*net.IPNet{{
IP: ipSlice,
IP: ip,
Mask: subnet.Mask,
}},
NotBefore: time.Now(),
@ -96,7 +92,7 @@ func NewHostCert(
// NewHostCredentials generates a new key/cert for a nebula host using the CA
// key which will be found in the adminFS.
func NewHostCredentials(
caCreds CACredentials, hostName HostName, ip netip.Addr,
caCreds CACredentials, hostName string, ip net.IP,
) (
pub HostPublicCredentials, priv HostPrivateCredentials, err error,
) {
@ -129,7 +125,7 @@ func NewHostCredentials(
// NewCACredentials generates a CACredentials. The domain should be the network's root domain,
// and is included in the signing certificate's Name field.
func NewCACredentials(domain string, subnet IPNet) (CACredentials, error) {
func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
var (
signingPubKey, signingPrivKey = GenerateSigningPair()
now = time.Now()
@ -139,7 +135,7 @@ func NewCACredentials(domain string, subnet IPNet) (CACredentials, error) {
caCert := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: fmt.Sprintf("%s isle root cert", domain),
Subnets: []*net.IPNet{(*net.IPNet)(&subnet)},
Subnets: []*net.IPNet{subnet},
NotBefore: now,
NotAfter: expireAt,
PublicKey: signingPubKey,

View File

@ -1,20 +1,22 @@
package nebula
import "net/netip"
import (
"net"
)
var (
ip netip.Addr
ipNet IPNet
ip net.IP
ipNet *net.IPNet
caCredsA, caCredsB CACredentials
hostPubCredsA, hostPubCredsB HostPublicCredentials
hostPrivCredsA, hostPrivCredsB HostPrivateCredentials
)
func init() {
ip = netip.MustParseAddr("192.168.0.1")
var err error
if err := ipNet.UnmarshalText([]byte("192.168.0.0/24")); err != nil {
ip, ipNet, err = net.ParseCIDR("192.168.0.1/24")
if err != nil {
panic(err)
}

View File

@ -1,9 +1,7 @@
# shellcheck source=../../utils/with-1-data-1-empty-node-network.sh
source "$UTILS"/with-1-data-1-empty-node-network.sh
# TODO when primus creates secondus' bootstrap it should write the new host to
# its own bootstrap, as well as to garage.
as_secondus
as_primus
hosts="$(isle hosts list)"
[ "$(echo "$hosts" | jq -r '.[0].Name')" = "primus" ]

View File

@ -2,8 +2,6 @@ set -e
base="shared/1-data-1-empty"
ipNet="10.6.9.0/24"
primus_base="$base/primus"
primus_ip="10.6.9.1"
@ -62,7 +60,7 @@ EOF
isle network create \
--domain shared.test \
--hostname primus \
--ip-net "$ipNet" \
--ip-net "$current_ip/24" \
--name "testing" \
> admin.json