Compare commits

..

9 Commits

29 changed files with 566 additions and 428 deletions

View File

@ -105,7 +105,4 @@ Documentation for devs:
Besides documentation, there are a few other pages which might be useful: Besides documentation, there are a few other pages which might be useful:
* [Roadmap][roadmap]
* [Glossary](docs/glossary.md) * [Glossary](docs/glossary.md)
[roadmap]: docs/roadmap.md

View File

@ -1,128 +0,0 @@
# Roadmap
The following are rough outlines of upcoming work on the roadmap, roughly in the
order they will be implemented.
## Main quest
These items are listed more or less in the order they need to be completed, as
they generally depend on the items previous to them.
### Windows Support + GUI
Support for Windows is a must. This requirement also includes a simple GUI,
which would essentially act as a thin layer on top of `daemon.yml` to start
with.
Depending on difficulty level, OSX support might be added at this stage as well.
### NATS
Garage is currently used to handle eventually-consistent persistent storage, but
there is no mechanism for inter-host realtime communication as of yet. NATS
would be a good candidate for this, as it uses a gossip protocol which does not
require a central coordinator (I don't think), and is well supported.
### Integration of [Caddy](https://caddyserver.com/docs/)
Integration of Caddy's will require some plugins to be developed. We want Caddy
to be able to store cert information in S3 (garage), so that all isle lighthouse
nodes can potentially become gateways as well. Once done, it would be possible
for lighthouses to forward public traffic to inner nodes.
It should also be possible for users within the network to take use lighthouse
Caddy's to host their websites (and eventually gemini capsules) for them.
Most likely this integration will require NATS as well, to coordinate cache
invalidation and cert refreshing.
### Invitation code bootstrapping
Once an HTTP gateway/load-balancer is set up it should be possible to do host
bootstrapping using invite codes rather than manually giving new users bootstrap
files. The bootstrap file would be stored, encrypted, in garage, with the invite
code being able to both identify and decrypt it. To instantiate a host, the user
only needs to input the network domain name and the invite code.
### FUSE Mount
KBFS style. Every user should be able to mount virtual directories to their host
which correspond to various buckets in garage.
- "public": editable amongst all users on the host, shared publicly via HTTP
gateway.
- "protected": editable amongst all users on the host, but not accessible
outside the network.
- "private": only accessible to a particular user (client-side encrypted).
Whether it's necessary to support directories which are shared only between
specific users remains to be seen. The identification of a single "user" between
different hosts is also an unsolved problem.
## Side quests
These items aren't necessarily required by the main quest, and aren't dependent
on any other items being completed. They are nice-to-haves that we do want to
eventually complete, but aren't the main focus.
### Design System
It would be great to get some help from a designer or otherwise
artistically-minded person to create some kind of design framework which could
be used across publicly-facing frontends. Such a system would provide a simple
but cohesive vision for how things should look, include:
- Color schemes
- Fonts and text decoration in different situations
- Some simple, reusable layout templates (splash page, documentation, form)
- Basic components like tables, lists, media, etc..
### DHCP
Currently all hosts require a static IP to be reserved by the admin. Nebula may
support DHCP already, but if it doesn't we should look into how this could be
accomplished. Depending on how reliable DNS support is it may be possible to use
DHCP for all non-lighthouse hosts, which would be excellent.
### IPv6 network ranges
It should theoretically be possible for the internal network IP range to be on
IPv6 rather than IPv4. This may be a simple matter of just testing it to confirm
it works.
### Proper Linux Packages
Rather than distributing raw binaries for Linux we should instead be
distributing actual packages.
* deb files for debian/ubuntu
* PKGBUILD for arch (done)
* rpm for fedora?
* flatpak?
This will allow for properly setting capabilities for the binary at install
time, so that it can be run as non-root, and installing any necessary `.desktop`
files so that it can be run as a GUI application.
### Mobile app
To start with a simple mobile app which provided connectivity to the network
would be great. We are not able to use the existing nebula mobile app because it
is not actually open-source, but we can at least use it as a reference to see
how this can be accomplished.
### DNS/Firewall Configuration
Ideally Isle could detect the DNS/firewall subsystems being used on a per-OS
basis and configure them as needed. This would be simplify necessary
documentation and setup steps for operators.
### Plugins
It would not be difficult to spec out a plugin system using nix commands.
Existing components could be rigged to use this plugin system, and we could then
use the system to add future components which might prove useful. Once the
project is public such a system would be much appreciated I think, as it would
let other groups rig their binaries with all sorts of new functionality.

View File

@ -47,6 +47,10 @@ type Bootstrap struct {
} }
// New initializes and returns a new Bootstrap file for a new host. // New initializes and returns a new Bootstrap file for a new host.
//
// TODO in the resulting bootstrap only include this host and hosts which are
// necessary for connecting to nebula/garage. Remember to immediately re-poll
// garage for the full hosts list during network joining.
func New( func New(
caCreds nebula.CACredentials, caCreds nebula.CACredentials,
adminCreationParams CreationParams, adminCreationParams CreationParams,

View File

@ -6,8 +6,7 @@ import (
) )
func (ctx subCmdCtx) getHosts() (daemon.GetHostsResult, error) { func (ctx subCmdCtx) getHosts() (daemon.GetHostsResult, error) {
var res daemon.GetHostsResult res, err := ctx.daemonRPC.GetHosts(ctx)
err := ctx.daemonRCPClient.Call(ctx.ctx, &res, "GetHosts", nil)
if err != nil { if err != nil {
return daemon.GetHostsResult{}, fmt.Errorf("calling GetHosts: %w", err) return daemon.GetHostsResult{}, fmt.Errorf("calling GetHosts: %w", err)
} }

View File

@ -10,12 +10,16 @@ import (
"dev.mediocregopher.com/mediocre-go-lib.git/mlog" "dev.mediocregopher.com/mediocre-go-lib.git/mlog"
) )
// TODO it would be good to have an `isle daemon config-check` kind of command,
// which could be run prior to a systemd service restart to make sure we don't
// restart the service into a configuration that will definitely fail.
var subCmdDaemon = subCmd{ var subCmdDaemon = subCmd{
name: "daemon", name: "daemon",
descr: "Runs the isle daemon (Default if no sub-command given)", descr: "Runs the isle daemon (Default if no sub-command given)",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
flags := subCmdCtx.flagSet(false) flags := ctx.flagSet(false)
daemonConfigPath := flags.StringP( daemonConfigPath := flags.StringP(
"config-path", "c", "", "config-path", "c", "",
@ -32,12 +36,10 @@ var subCmdDaemon = subCmd{
`Maximum log level which should be output. Values can be "debug", "info", "warn", "error", "fatal". Does not apply to sub-processes`, `Maximum log level which should be output. Values can be "debug", "info", "warn", "error", "fatal". Does not apply to sub-processes`,
) )
if err := flags.Parse(subCmdCtx.args); err != nil { if err := flags.Parse(ctx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
ctx := subCmdCtx.ctx
if *dumpConfig { if *dumpConfig {
return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath) return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
} }
@ -47,7 +49,7 @@ var subCmdDaemon = subCmd{
return fmt.Errorf("couldn't parse log level %q", *logLevelStr) return fmt.Errorf("couldn't parse log level %q", *logLevelStr)
} }
logger := subCmdCtx.logger.WithMaxLevel(logLevel.Int()) logger := ctx.logger.WithMaxLevel(logLevel.Int())
// TODO check that daemon is either running as root, or that the // TODO check that daemon is either running as root, or that the
// required linux capabilities are set. // required linux capabilities are set.

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"isle/daemon" "isle/daemon"
"isle/daemon/jsonrpc2" "isle/daemon/jsonrpc2"
"net" "net"
@ -17,11 +18,25 @@ import (
const daemonHTTPRPCPath = "/rpc/v0.json" const daemonHTTPRPCPath = "/rpc/v0.json"
func newHTTPServer( func newHTTPServer(
ctx context.Context, logger *mlog.Logger, rpc *daemon.RPC, ctx context.Context, logger *mlog.Logger, rpc daemon.RPC,
) ( ) (
*http.Server, error, *http.Server, error,
) { ) {
socketPath := daemon.HTTPSocketPath() socketPath := daemon.HTTPSocketPath()
ctx = mctx.Annotate(ctx, "socketPath", socketPath)
if err := os.Remove(socketPath); errors.Is(err, fs.ErrNotExist) {
// No problem
} else if err != nil {
return nil, fmt.Errorf(
"removing %q prior to listening: %w", socketPath, err,
)
} else {
logger.WarnString(
ctx, "Deleted existing socket file prior to listening, it's possible a previous daemon failed to shutdown gracefully",
)
}
l, err := net.Listen("unix", socketPath) l, err := net.Listen("unix", socketPath)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
@ -35,7 +50,6 @@ func newHTTPServer(
) )
} }
ctx = mctx.Annotate(ctx, "httpAddr", l.Addr().String())
logger.Info(ctx, "HTTP server socket created") logger.Info(ctx, "HTTP server socket created")
rpcHandler := jsonrpc2.Chain( rpcHandler := jsonrpc2.Chain(

View File

@ -3,20 +3,37 @@ package main
import ( import (
"encoding" "encoding"
"fmt" "fmt"
"isle/nebula"
"net/netip"
) )
type textUnmarshalerFlag struct { type textUnmarshaler[T any] interface {
inner interface {
encoding.TextUnmarshaler encoding.TextUnmarshaler
*T
}
type textUnmarshalerFlag[T encoding.TextMarshaler, P textUnmarshaler[T]] struct {
V T
}
func (f *textUnmarshalerFlag[T, P]) Set(v string) error {
return P(&(f.V)).UnmarshalText([]byte(v))
}
func (f *textUnmarshalerFlag[T, P]) String() string {
b, err := f.V.MarshalText()
if err != nil {
panic(fmt.Sprintf("calling MarshalText on %#v: %v", f.V, err))
} }
return string(b)
} }
func (f textUnmarshalerFlag) Set(v string) error { func (f *textUnmarshalerFlag[T, P]) Type() string { return "string" }
return f.inner.UnmarshalText([]byte(v))
}
func (f textUnmarshalerFlag) String() string { ////////////////////////////////////////////////////////////////////////////////
return fmt.Sprint(f.inner)
}
func (f textUnmarshalerFlag) Type() string { return "string" } type (
hostNameFlag = textUnmarshalerFlag[nebula.HostName, *nebula.HostName]
ipNetFlag = textUnmarshalerFlag[nebula.IPNet, *nebula.IPNet]
ipFlag = textUnmarshalerFlag[netip.Addr, *netip.Addr]
)

View File

@ -6,8 +6,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"syscall" "syscall"
"isle/daemon"
) )
// minio-client keeps a configuration directory which contains various pieces of // minio-client keeps a configuration directory which contains various pieces of
@ -35,9 +33,9 @@ func initMCConfigDir() (string, error) {
var subCmdGarageMC = subCmd{ var subCmdGarageMC = subCmd{
name: "mc", name: "mc",
descr: "Runs the mc (minio-client) binary. The isle garage can be accessed under the `garage` alias", descr: "Runs the mc (minio-client) binary. The isle garage can be accessed under the `garage` alias",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
flags := subCmdCtx.flagSet(true) flags := ctx.flagSet(true)
keyID := flags.StringP( keyID := flags.StringP(
"key-id", "i", "", "key-id", "i", "",
@ -49,14 +47,11 @@ var subCmdGarageMC = subCmd{
"Optional key secret to use, defaults to that of the shared global key", "Optional key secret to use, defaults to that of the shared global key",
) )
if err := flags.Parse(subCmdCtx.args); err != nil { if err := flags.Parse(ctx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
var clientParams daemon.GarageClientParams clientParams, err := ctx.daemonRPC.GetGarageClientParams(ctx)
err := subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
)
if err != nil { if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err) return fmt.Errorf("calling GetGarageClientParams: %w", err)
} }
@ -118,12 +113,9 @@ var subCmdGarageMC = subCmd{
var subCmdGarageCLI = subCmd{ var subCmdGarageCLI = subCmd{
name: "cli", name: "cli",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon", descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var clientParams daemon.GarageClientParams clientParams, err := ctx.daemonRPC.GetGarageClientParams(ctx)
err := subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx, &clientParams, "GetGarageClientParams", nil,
)
if err != nil { if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err) return fmt.Errorf("calling GetGarageClientParams: %w", err)
} }
@ -134,7 +126,7 @@ var subCmdGarageCLI = subCmd{
var ( var (
binPath = binPath("garage") binPath = binPath("garage")
args = append([]string{"garage"}, subCmdCtx.args...) args = append([]string{"garage"}, ctx.args...)
cliEnv = append( cliEnv = append(
os.Environ(), os.Environ(),
"GARAGE_RPC_HOST="+clientParams.Peer.RPCPeerAddr(), "GARAGE_RPC_HOST="+clientParams.Peer.RPCPeerAddr(),
@ -156,8 +148,8 @@ var subCmdGarageCLI = subCmd{
var subCmdGarage = subCmd{ var subCmdGarage = subCmd{
name: "garage", name: "garage",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon", descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
return subCmdCtx.doSubCmd( return ctx.doSubCmd(
subCmdGarageCLI, subCmdGarageCLI,
subCmdGarageMC, subCmdGarageMC,
) )

View File

@ -7,31 +7,27 @@ import (
"isle/bootstrap" "isle/bootstrap"
"isle/daemon" "isle/daemon"
"isle/jsonutil" "isle/jsonutil"
"isle/nebula"
"net/netip"
"os" "os"
"sort" "sort"
) )
var subCmdHostsCreate = subCmd{ var subCmdHostCreate = subCmd{
name: "create", name: "create",
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout", descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( var (
flags = subCmdCtx.flagSet(false) flags = ctx.flagSet(false)
hostName nebula.HostName hostName hostNameFlag
ip netip.Addr ip ipFlag
) )
hostNameF := flags.VarPF( hostNameF := flags.VarPF(
textUnmarshalerFlag{&hostName}, &hostName,
"hostname", "h", "hostname", "n",
"Name of the host to generate bootstrap.json for", "Name of the host to generate bootstrap.json for",
) )
flags.VarP( flags.VarP(&ip, "ip", "i", "IP of the new host. An available IP will be chosen if none is given.")
textUnmarshalerFlag{&ip}, "ip", "i", "IP of the new host",
)
canCreateHosts := flags.Bool( canCreateHosts := flags.Bool(
"can-create-hosts", "can-create-hosts",
@ -39,7 +35,7 @@ var subCmdHostsCreate = subCmd{
"The new host should have the ability to create hosts too", "The new host should have the ability to create hosts too",
) )
if err := flags.Parse(subCmdCtx.args); err != nil { if err := flags.Parse(ctx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -47,18 +43,11 @@ var subCmdHostsCreate = subCmd{
return errors.New("--hostname is required") return errors.New("--hostname is required")
} }
var res daemon.CreateHostResult res, err := ctx.daemonRPC.CreateHost(
err := subCmdCtx.daemonRCPClient.Call( ctx, hostName.V, daemon.CreateHostOpts{
subCmdCtx.ctx, IP: ip.V,
&res,
"CreateHost",
daemon.CreateHostRequest{
HostName: hostName,
Opts: daemon.CreateHostOpts{
IP: ip,
CanCreateHosts: *canCreateHosts, CanCreateHosts: *canCreateHosts,
}, },
},
) )
if err != nil { if err != nil {
return fmt.Errorf("calling CreateHost: %w", err) return fmt.Errorf("calling CreateHost: %w", err)
@ -68,11 +57,11 @@ var subCmdHostsCreate = subCmd{
}, },
} }
var subCmdHostsList = subCmd{ var subCmdHostList = subCmd{
name: "list", name: "list",
descr: "Lists all hosts in the network, and their IPs", descr: "Lists all hosts in the network, and their IPs",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
hostsRes, err := subCmdCtx.getHosts() hostsRes, err := ctx.getHosts()
if err != nil { if err != nil {
return fmt.Errorf("calling GetHosts: %w", err) return fmt.Errorf("calling GetHosts: %w", err)
} }
@ -104,22 +93,22 @@ var subCmdHostsList = subCmd{
}, },
} }
var subCmdHostsRemove = subCmd{ var subCmdHostRemove = subCmd{
name: "remove", name: "remove",
descr: "Removes a host from the network", descr: "Removes a host from the network",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( var (
flags = subCmdCtx.flagSet(false) flags = ctx.flagSet(false)
hostName nebula.HostName hostName hostNameFlag
) )
hostNameF := flags.VarPF( hostNameF := flags.VarPF(
textUnmarshalerFlag{&hostName}, &hostName,
"hostname", "h", "hostname", "n",
"Name of the host to remove", "Name of the host to remove",
) )
if err := flags.Parse(subCmdCtx.args); err != nil { if err := flags.Parse(ctx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -127,11 +116,7 @@ var subCmdHostsRemove = subCmd{
return errors.New("--hostname is required") return errors.New("--hostname is required")
} }
err := subCmdCtx.daemonRCPClient.Call( _, err := ctx.daemonRPC.RemoveHost(ctx, hostName.V)
subCmdCtx.ctx, nil, "RemoveHost", daemon.RemoveHostRequest{
HostName: hostName,
},
)
if err != nil { if err != nil {
return fmt.Errorf("calling RemoveHost: %w", err) return fmt.Errorf("calling RemoveHost: %w", err)
} }
@ -140,14 +125,15 @@ var subCmdHostsRemove = subCmd{
}, },
} }
var subCmdHosts = subCmd{ var subCmdHost = subCmd{
name: "hosts", name: "host",
plural: "s",
descr: "Sub-commands having to do with configuration of hosts in the network", descr: "Sub-commands having to do with configuration of hosts in the network",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
return subCmdCtx.doSubCmd( return ctx.doSubCmd(
subCmdHostsCreate, subCmdHostCreate,
subCmdHostsRemove, subCmdHostRemove,
subCmdHostsList, subCmdHostList,
) )
}, },
} }

View File

@ -57,13 +57,13 @@ func main() {
}() }()
err := subCmdCtx{ err := subCmdCtx{
Context: ctx,
args: os.Args[1:], args: os.Args[1:],
ctx: ctx,
logger: logger, logger: logger,
}.doSubCmd( }.doSubCmd(
subCmdDaemon, subCmdDaemon,
subCmdGarage, subCmdGarage,
subCmdHosts, subCmdHost,
subCmdNebula, subCmdNebula,
subCmdNetwork, subCmdNetwork,
subCmdVersion, subCmdVersion,

View File

@ -3,7 +3,6 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"isle/daemon"
"isle/jsonutil" "isle/jsonutil"
"isle/nebula" "isle/nebula"
"os" "os"
@ -12,15 +11,15 @@ import (
var subCmdNebulaCreateCert = subCmd{ var subCmdNebulaCreateCert = subCmd{
name: "create-cert", name: "create-cert",
descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout", descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( var (
flags = subCmdCtx.flagSet(false) flags = ctx.flagSet(false)
hostName nebula.HostName hostName hostNameFlag
) )
hostNameF := flags.VarPF( hostNameF := flags.VarPF(
textUnmarshalerFlag{&hostName}, &hostName,
"hostname", "h", "hostname", "n",
"Name of the host to generate a certificate for", "Name of the host to generate a certificate for",
) )
@ -29,7 +28,7 @@ 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.`,
) )
if err := flags.Parse(subCmdCtx.args); err != nil { if err := flags.Parse(ctx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -47,21 +46,14 @@ var subCmdNebulaCreateCert = subCmd{
return fmt.Errorf("unmarshaling public key as PEM: %w", err) return fmt.Errorf("unmarshaling public key as PEM: %w", err)
} }
var res daemon.CreateNebulaCertificateResult res, err := ctx.daemonRPC.CreateNebulaCertificate(
err = subCmdCtx.daemonRCPClient.Call( ctx, hostName.V, hostPub,
subCmdCtx.ctx,
&res,
"CreateNebulaCertificate",
daemon.CreateNebulaCertificateRequest{
HostName: hostName,
HostEncryptingPublicKey: hostPub,
},
) )
if err != nil { if err != nil {
return fmt.Errorf("calling CreateNebulaCertificate: %w", err) return fmt.Errorf("calling CreateNebulaCertificate: %w", err)
} }
nebulaHostCertPEM, err := res.HostNebulaCertifcate.Unwrap().MarshalToPEM() nebulaHostCertPEM, err := res.HostNebulaCertificate.Unwrap().MarshalToPEM()
if err != nil { if err != nil {
return fmt.Errorf("marshaling cert to PEM: %w", err) return fmt.Errorf("marshaling cert to PEM: %w", err)
} }
@ -77,22 +69,19 @@ var subCmdNebulaCreateCert = subCmd{
var subCmdNebulaShow = subCmd{ var subCmdNebulaShow = subCmd{
name: "show", name: "show",
descr: "Writes nebula network information to stdout in JSON format", descr: "Writes nebula network information to stdout in JSON format",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
flags := subCmdCtx.flagSet(false) flags := ctx.flagSet(false)
if err := flags.Parse(subCmdCtx.args); err != nil { if err := flags.Parse(ctx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
hosts, err := subCmdCtx.getHosts() hosts, err := ctx.getHosts()
if err != nil { if err != nil {
return fmt.Errorf("getting hosts: %w", err) return fmt.Errorf("getting hosts: %w", err)
} }
var caPublicCreds nebula.CAPublicCredentials caPublicCreds, err := ctx.daemonRPC.GetNebulaCAPublicCredentials(ctx)
err = subCmdCtx.daemonRCPClient.Call(
subCmdCtx.ctx, &caPublicCreds, "GetNebulaCAPublicCredentials", nil,
)
if err != nil { if err != nil {
return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err) return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
} }
@ -145,8 +134,8 @@ var subCmdNebulaShow = subCmd{
var subCmdNebula = subCmd{ var subCmdNebula = subCmd{
name: "nebula", name: "nebula",
descr: "Sub-commands related to the nebula VPN", descr: "Sub-commands related to the nebula VPN",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
return subCmdCtx.doSubCmd( return ctx.doSubCmd(
subCmdNebulaCreateCert, subCmdNebulaCreateCert,
subCmdNebulaShow, subCmdNebulaShow,
) )

View File

@ -10,48 +10,50 @@ import (
var subCmdNetworkCreate = subCmd{ var subCmdNetworkCreate = subCmd{
name: "create", name: "create",
descr: "Create's a new network, with this host being the first host in that network.", descr: "Create's a new network, with this host being the first host in that network.",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( var (
ctx = subCmdCtx.ctx flags = ctx.flagSet(false)
flags = subCmdCtx.flagSet(false) ipNet ipNetFlag
req daemon.CreateNetworkRequest hostName hostNameFlag
) )
flags.StringVarP( name := flags.StringP(
&req.Name, "name", "n", "", "name", "N", "",
"Human-readable name to identify the network as.", "Human-readable name to identify the network as.",
) )
flags.StringVarP( domain := flags.StringP(
&req.Domain, "domain", "d", "", "domain", "d", "",
"Domain name that should be used as the root domain in the network.", "Domain name that should be used as the root domain in the network.",
) )
ipNetF := flags.VarPF( ipNetF := flags.VarPF(
textUnmarshalerFlag{&req.IPNet}, "ip-net", "i", &ipNet, "ip-net", "i",
`An IP subnet, in CIDR form, which will be the overall range of`+ `An IP subnet, in CIDR form, which will be the overall range of`+
` possible IPs in the network. The first IP in this network`+ ` possible IPs in the network. The first IP in this network`+
` range will become this first host's IP.`, ` range will become this first host's IP.`,
) )
hostNameF := flags.VarPF( hostNameF := flags.VarPF(
textUnmarshalerFlag{&req.HostName}, &hostName,
"hostname", "h", "hostname", "n",
"Name of this host, which will be the first host in the network", "Name of this host, which will be the first host in the network",
) )
if err := flags.Parse(subCmdCtx.args); err != nil { if err := flags.Parse(ctx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
if req.Name == "" || if *name == "" ||
req.Domain == "" || *domain == "" ||
!ipNetF.Changed || !ipNetF.Changed ||
!hostNameF.Changed { !hostNameF.Changed {
return errors.New("--name, --domain, --ip-net, and --hostname are required") return errors.New("--name, --domain, --ip-net, and --hostname are required")
} }
err := subCmdCtx.daemonRCPClient.Call(ctx, nil, "CreateNetwork", req) _, err := ctx.daemonRPC.CreateNetwork(
ctx, *name, *domain, ipNet.V, hostName.V,
)
if err != nil { if err != nil {
return fmt.Errorf("creating network: %w", err) return fmt.Errorf("creating network: %w", err)
} }
@ -63,17 +65,15 @@ var subCmdNetworkCreate = subCmd{
var subCmdNetworkJoin = subCmd{ var subCmdNetworkJoin = subCmd{
name: "join", name: "join",
descr: "Joins this host to an existing network", descr: "Joins this host to an existing network",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
var ( var (
ctx = subCmdCtx.ctx flags = ctx.flagSet(false)
flags = subCmdCtx.flagSet(false) bootstrapPath = flags.StringP(
)
bootstrapPath := flags.StringP(
"bootstrap-path", "b", "", "Path to a bootstrap.json file.", "bootstrap-path", "b", "", "Path to a bootstrap.json file.",
) )
)
if err := flags.Parse(subCmdCtx.args); err != nil { if err := flags.Parse(ctx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
@ -88,17 +88,16 @@ var subCmdNetworkJoin = subCmd{
) )
} }
return subCmdCtx.daemonRCPClient.Call( _, err := ctx.daemonRPC.JoinNetwork(ctx, newBootstrap)
ctx, nil, "JoinNetwork", newBootstrap, return err
)
}, },
} }
var subCmdNetwork = subCmd{ var subCmdNetwork = subCmd{
name: "network", name: "network",
descr: "Sub-commands related to network membership", descr: "Sub-commands related to network membership",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
return subCmdCtx.doSubCmd( return ctx.doSubCmd(
subCmdNetworkCreate, subCmdNetworkCreate,
subCmdNetworkJoin, subCmdNetworkJoin,
) )

View File

@ -12,21 +12,41 @@ import (
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
type flagSet struct {
*pflag.FlagSet
}
func (fs flagSet) Parse(args []string) error {
fs.VisitAll(func(f *pflag.Flag) {
if f.Shorthand == "h" {
panic(fmt.Sprintf("flag %+v has reserved shorthand `-h`", f))
}
if f.Name == "help" {
panic(fmt.Sprintf("flag %+v has reserved name `--help`", f))
}
})
return fs.FlagSet.Parse(args)
}
// subCmdCtx contains all information available to a subCmd's do method. // subCmdCtx contains all information available to a subCmd's do method.
type subCmdCtx struct { type subCmdCtx struct {
context.Context
subCmd subCmd // the subCmd itself subCmd subCmd // the subCmd itself
args []string // command-line arguments, excluding the subCmd itself. args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one subCmdNames []string // names of subCmds so far, including this one
ctx context.Context
logger *mlog.Logger logger *mlog.Logger
daemonRCPClient jsonrpc2.Client daemonRPC daemon.RPC
} }
type subCmd struct { type subCmd struct {
name string name string
descr string descr string
do func(subCmdCtx) error do func(subCmdCtx) error
// If set then the name will be allowed to be suffixed with this string.
plural string
} }
func (ctx subCmdCtx) usagePrefix() string { func (ctx subCmdCtx) usagePrefix() string {
@ -39,7 +59,7 @@ func (ctx subCmdCtx) usagePrefix() string {
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr) return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
} }
func (ctx subCmdCtx) flagSet(withPassthrough bool) *pflag.FlagSet { func (ctx subCmdCtx) flagSet(withPassthrough bool) flagSet {
flags := pflag.NewFlagSet(ctx.subCmd.name, pflag.ExitOnError) flags := pflag.NewFlagSet(ctx.subCmd.name, pflag.ExitOnError)
flags.Usage = func() { flags.Usage = func() {
@ -58,7 +78,7 @@ func (ctx subCmdCtx) flagSet(withPassthrough bool) *pflag.FlagSet {
os.Stderr.Sync() os.Stderr.Sync()
os.Exit(2) os.Exit(2)
} }
return flags return flagSet{flags}
} }
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error { func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
@ -76,7 +96,11 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n") fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
for _, subCmd := range subCmds { for _, subCmd := range subCmds {
fmt.Fprintf(os.Stderr, " %s\t%s\n", subCmd.name, subCmd.descr) name := subCmd.name
if subCmd.plural != "" {
name += "(" + subCmd.plural + ")"
}
fmt.Fprintf(os.Stderr, " %s\t%s\n", name, subCmd.descr)
} }
fmt.Fprintf(os.Stderr, "\n") fmt.Fprintf(os.Stderr, "\n")
@ -92,8 +116,10 @@ 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
if subCmd.plural != "" {
subCmdsMap[subCmd.name+subCmd.plural] = subCmd
}
} }
subCmdName, args := args[0], args[1:] subCmdName, args := args[0], args[1:]
@ -103,17 +129,17 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
printUsageExit(subCmdName) printUsageExit(subCmdName)
} }
daemonRCPClient := jsonrpc2.NewUnixHTTPClient( daemonRPC := daemon.RPCFromClient(
daemon.HTTPSocketPath(), daemonHTTPRPCPath, jsonrpc2.NewUnixHTTPClient(daemon.HTTPSocketPath(), daemonHTTPRPCPath),
) )
err := subCmd.do(subCmdCtx{ err := subCmd.do(subCmdCtx{
Context: ctx.Context,
subCmd: subCmd, subCmd: subCmd,
args: args, args: args,
subCmdNames: append(ctx.subCmdNames, subCmdName), subCmdNames: append(ctx.subCmdNames, subCmdName),
ctx: ctx.ctx,
logger: ctx.logger, logger: ctx.logger,
daemonRCPClient: daemonRCPClient, daemonRPC: daemonRPC,
}) })
if err != nil { if err != nil {

View File

@ -9,7 +9,7 @@ import (
var subCmdVersion = subCmd{ var subCmdVersion = subCmd{
name: "version", name: "version",
descr: "Dumps version and build info to stdout", descr: "Dumps version and build info to stdout",
do: func(subCmdCtx subCmdCtx) error { do: func(ctx subCmdCtx) error {
versionPath := filepath.Join(envAppDirPath, "share/version") versionPath := filepath.Join(envAppDirPath, "share/version")

View File

@ -75,6 +75,14 @@ func dnsmasqPmuxProcConfig(
Cmd: filepath.Join(binDirPath, "dnsmasq"), Cmd: filepath.Join(binDirPath, "dnsmasq"),
Args: []string{"-d", "-C", confPath}, Args: []string{"-d", "-C", confPath},
StartAfterFunc: func(ctx context.Context) error { StartAfterFunc: func(ctx context.Context) error {
// TODO consider a shared dnsmasq across all the daemon's networks.
// This would have a few benefits:
// - Less processes, less problems
// - Less configuration for the user in the case of more than one
// network.
// - Can listen on 127.0.0.x:53, rather than on the nebula address.
// This allows DNS to come up before nebula, which is helpful when
// nebula depends on DNS.
return waitForNebula(ctx, logger, hostBootstrap) return waitForNebula(ctx, logger, hostBootstrap)
}, },
}, nil }, nil

108
go/daemon/client.go Normal file
View File

@ -0,0 +1,108 @@
// Code generated by gowrap. DO NOT EDIT.
// template: jsonrpc2/client_gen.tpl
// gowrap: http://github.com/hexdigest/gowrap
package daemon
//go:generate gowrap gen -p isle/daemon -i RPC -t jsonrpc2/client_gen.tpl -o client.go -l ""
import (
"context"
"isle/daemon/jsonrpc2"
"isle/nebula"
)
type rpcClient struct {
client jsonrpc2.Client
}
// RPCFromClient wraps a Client so that it implements the
// RPC interface.
func RPCFromClient(client jsonrpc2.Client) RPC {
return &rpcClient{client}
}
func (c *rpcClient) CreateHost(ctx context.Context, hostName nebula.HostName, opts CreateHostOpts) (c2 CreateHostResult, err error) {
err = c.client.Call(
ctx,
&c2,
"CreateHost",
hostName,
opts,
)
return
}
func (c *rpcClient) CreateNebulaCertificate(ctx context.Context, hostName nebula.HostName, hostEncryptingPublicKey nebula.EncryptingPublicKey) (c2 CreateNebulaCertificateResult, err error) {
err = c.client.Call(
ctx,
&c2,
"CreateNebulaCertificate",
hostName,
hostEncryptingPublicKey,
)
return
}
func (c *rpcClient) CreateNetwork(ctx context.Context, name string, domain string, ipNet nebula.IPNet, hostName nebula.HostName) (st1 struct {
}, err error) {
err = c.client.Call(
ctx,
&st1,
"CreateNetwork",
name,
domain,
ipNet,
hostName,
)
return
}
func (c *rpcClient) GetGarageClientParams(ctx context.Context) (g1 GarageClientParams, err error) {
err = c.client.Call(
ctx,
&g1,
"GetGarageClientParams",
)
return
}
func (c *rpcClient) GetHosts(ctx context.Context) (g1 GetHostsResult, err error) {
err = c.client.Call(
ctx,
&g1,
"GetHosts",
)
return
}
func (c *rpcClient) GetNebulaCAPublicCredentials(ctx context.Context) (c2 nebula.CAPublicCredentials, err error) {
err = c.client.Call(
ctx,
&c2,
"GetNebulaCAPublicCredentials",
)
return
}
func (c *rpcClient) JoinNetwork(ctx context.Context, req JoiningBootstrap) (st1 struct {
}, err error) {
err = c.client.Call(
ctx,
&st1,
"JoinNetwork",
req,
)
return
}
func (c *rpcClient) RemoveHost(ctx context.Context, hostName nebula.HostName) (st1 struct {
}, err error) {
err = c.client.Call(
ctx,
&st1,
"RemoveHost",
hostName,
)
return
}

View File

@ -32,6 +32,8 @@ type CreateHostOpts struct {
// 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
// TODO add nebula cert tags
} }
// Daemon presents all functionality required for client frontends to interact // Daemon presents all functionality required for client frontends to interact
@ -85,6 +87,9 @@ type Daemon interface {
// existing host, given the public key for that host. This is currently // existing host, given the public key for that host. This is currently
// mostly useful for creating certs for mobile devices. // mostly useful for creating certs for mobile devices.
// //
// TODO replace this with CreateHostBootstrap, and the
// CreateNebulaCertificate RPC method can just pull cert out of that.
//
// Errors: // Errors:
// - ErrHostNotFound // - ErrHostNotFound
CreateNebulaCertificate( CreateNebulaCertificate(
@ -685,6 +690,7 @@ func (d *daemon) CreateHost(
) )
} }
} }
// TODO if the ip is given, check that it's not already in use.
caSigningPrivateKey, err := getNebulaCASigningPrivateKey( caSigningPrivateKey, err := getNebulaCASigningPrivateKey(
ctx, d.secretsStore, ctx, d.secretsStore,

View File

@ -15,5 +15,5 @@ type Client interface {
// //
// If an error result is returned from the server that will be returned as // If an error result is returned from the server that will be returned as
// an Error struct. // an Error struct.
Call(ctx context.Context, rcv any, method string, params any) error Call(ctx context.Context, rcv any, method string, params ...any) error
} }

View File

@ -0,0 +1,32 @@
import (
"isle/daemon/jsonrpc2"
)
{{ $t := printf "%sClient" (down .Interface.Name) }}
type {{$t}} struct {
client jsonrpc2.Client
}
// {{.Interface.Name}}FromClient wraps a Client so that it implements the
// {{.Interface.Name}} interface.
func {{.Interface.Name}}FromClient(client jsonrpc2.Client) {{.Interface.Name}} {
return &{{$t}}{client}
}
{{range $method := .Interface.Methods}}
func (c *{{$t}}) {{$method.Declaration}} {
{{- $ctx := (index $method.Params 0).Name}}
{{- $rcv := (index $method.Results 0).Name}}
{{- $err := (index $method.Results 1).Name}}
{{- $err}} = c.client.Call(
{{$ctx}},
&{{$rcv}},
"{{$method.Name}}",
{{- range $param := (slice $method.Params 1)}}
{{$param.Name}},
{{- end}}
)
return
}
{{end}}

View File

@ -49,7 +49,7 @@ func NewUnixHTTPClient(unixSocketPath, reqPath string) Client {
} }
func (c *httpClient) Call( func (c *httpClient) Call(
ctx context.Context, rcv any, method string, params any, ctx context.Context, rcv any, method string, params ...any,
) error { ) error {
var ( var (
body = new(bytes.Buffer) body = new(bytes.Buffer)

View File

@ -19,7 +19,7 @@ func NewReadWriterClient(rw io.ReadWriter) Client {
} }
func (c rwClient) Call( func (c rwClient) Call(
ctx context.Context, rcv any, method string, params any, ctx context.Context, rcv any, method string, params ...any,
) error { ) error {
id, err := encodeRequest(c.enc, method, params) id, err := encodeRequest(c.enc, method, params)
if err != nil { if err != nil {

View File

@ -18,14 +18,19 @@ type methodDispatchFunc func(context.Context, Request) (any, error)
func newMethodDispatchFunc( func newMethodDispatchFunc(
method reflect.Value, method reflect.Value,
) methodDispatchFunc { ) methodDispatchFunc {
paramT := method.Type().In(1) paramTs := make([]reflect.Type, method.Type().NumIn()-1)
return func(ctx context.Context, req Request) (any, error) { for i := range paramTs {
var ( paramTs[i] = method.Type().In(i + 1)
ctxV = reflect.ValueOf(ctx) }
paramPtrV = reflect.New(paramT)
)
err := json.Unmarshal(req.Params, paramPtrV.Interface()) return func(ctx context.Context, req Request) (any, error) {
callVals := make([]reflect.Value, 0, len(paramTs)+1)
callVals = append(callVals, reflect.ValueOf(ctx))
for i, paramT := range paramTs {
paramPtrV := reflect.New(paramT)
err := json.Unmarshal(req.Params[i], paramPtrV.Interface())
if err != nil { if err != nil {
// The JSON has already been validated, so this is not an // The JSON has already been validated, so this is not an
// errCodeParse situation. We assume it's an invalid param then, // errCodeParse situation. We assume it's an invalid param then,
@ -33,14 +38,17 @@ func newMethodDispatchFunc(
// returning an Error of its own. // returning an Error of its own.
if !errors.As(err, new(Error)) { if !errors.As(err, new(Error)) {
err = NewInvalidParamsError( err = NewInvalidParamsError(
"JSON unmarshaling params into %T: %v", paramT, err, "JSON unmarshaling param %d into %T: %v", i, paramT, err,
) )
} }
return nil, err return nil, err
} }
callVals = append(callVals, paramPtrV.Elem())
}
var ( var (
callResV = method.Call([]reflect.Value{ctxV, paramPtrV.Elem()}) callResV = method.Call(callVals)
resV = callResV[0] resV = callResV[0]
errV = callResV[1] errV = callResV[1]
) )
@ -86,7 +94,7 @@ func NewDispatchHandler(i any) Handler {
) )
if !method.IsExported() || if !method.IsExported() ||
methodT.NumIn() != 2 || methodT.NumIn() < 1 ||
methodT.In(0) != ctxT || methodT.In(0) != ctxT ||
methodT.NumOut() != 2 || methodT.NumOut() != 2 ||
methodT.Out(1) != errT { methodT.Out(1) != errT {

View File

@ -3,6 +3,7 @@ package jsonrpc2
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx" "dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog" "dev.mediocregopher.com/mediocre-go-lib.git/mlog"
@ -59,9 +60,14 @@ func NewMLogMiddleware(logger *mlog.Logger) Middleware {
) )
if logger.MaxLevel() >= mlog.LevelDebug.Int() { if logger.MaxLevel() >= mlog.LevelDebug.Int() {
ctx := mctx.Annotate( ctx := ctx
ctx, "rpcRequestParams", string(req.Params), for i := range req.Params {
ctx = mctx.Annotate(
ctx,
fmt.Sprintf("rpcRequestParam%d", i),
string(req.Params[i]),
) )
}
logger.Debug(ctx, "Handling RPC request") logger.Debug(ctx, "Handling RPC request")
} }

View File

@ -15,7 +15,7 @@ const version = "2.0"
type Request struct { type Request struct {
Version string `json:"jsonrpc"` // must be "2.0" Version string `json:"jsonrpc"` // must be "2.0"
Method string `json:"method"` Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"` Params []json.RawMessage `json:"params,omitempty"`
ID string `json:"id"` ID string `json:"id"`
} }
@ -37,13 +37,19 @@ func newID() string {
// encodeRequest writes a request to an io.Writer, returning the ID of the // encodeRequest writes a request to an io.Writer, returning the ID of the
// request. // request.
func encodeRequest( func encodeRequest(
enc *json.Encoder, method string, params any, enc *json.Encoder, method string, params []any,
) ( ) (
string, error, string, error,
) { ) {
paramsB, err := json.Marshal(params) var (
paramsBs = make([]json.RawMessage, len(params))
err error
)
for i := range params {
paramsBs[i], err = json.Marshal(params[i])
if err != nil { if err != nil {
return "", fmt.Errorf("encoding params as JSON: %w", err) return "", fmt.Errorf("encoding param %d as JSON: %w", i, err)
}
} }
var ( var (
@ -51,7 +57,7 @@ func encodeRequest(
reqEnvelope = Request{ reqEnvelope = Request{
Version: version, Version: version,
Method: method, Method: method,
Params: paramsB, Params: paramsBs,
ID: id, ID: id,
} }
) )

View File

@ -26,14 +26,22 @@ var ErrDivideByZero = Error{
type dividerImpl struct{} type dividerImpl struct{}
func (dividerImpl) Divide(ctx context.Context, p DivideParams) (int, error) { func (dividerImpl) Divide2(ctx context.Context, top, bottom int) (int, error) {
if p.Bottom == 0 { if bottom == 0 {
return 0, ErrDivideByZero return 0, ErrDivideByZero
} }
if p.Top%p.Bottom != 0 { if top%bottom != 0 {
return 0, errors.New("numbers don't divide evenly, cannot compute!") return 0, errors.New("numbers don't divide evenly, cannot compute!")
} }
return p.Top / p.Bottom, nil return top / bottom, nil
}
func (i dividerImpl) Noop(ctx context.Context) (int, error) {
return 1, nil
}
func (i dividerImpl) Divide(ctx context.Context, p DivideParams) (int, error) {
return i.Divide2(ctx, p.Top, p.Bottom)
} }
func (dividerImpl) Hidden(ctx context.Context, p struct{}) (int, error) { func (dividerImpl) Hidden(ctx context.Context, p struct{}) (int, error) {
@ -41,6 +49,8 @@ func (dividerImpl) Hidden(ctx context.Context, p struct{}) (int, error) {
} }
type divider interface { type divider interface {
Noop(ctx context.Context) (int, error)
Divide2(ctx context.Context, top, bottom int) (int, error)
Divide(ctx context.Context, p DivideParams) (int, error) Divide(ctx context.Context, p DivideParams) (int, error)
} }
@ -82,6 +92,26 @@ func testClient(t *testing.T, client Client) {
} }
}) })
t.Run("success/multiple_params", func(t *testing.T) {
var res int
err := client.Call(ctx, &res, "Divide2", 6, 3)
if err != nil {
t.Fatal(err)
} else if res != 2 {
t.Fatalf("expected 2, got %d", res)
}
})
t.Run("success/no_params", func(t *testing.T) {
var res int
err := client.Call(ctx, &res, "Noop")
if err != nil {
t.Fatal(err)
} else if res != 1 {
t.Fatalf("expected 1, got %d", res)
}
})
t.Run("err/application", func(t *testing.T) { t.Run("err/application", func(t *testing.T) {
err := client.Call(ctx, nil, "Divide", DivideParams{}) err := client.Call(ctx, nil, "Divide", DivideParams{})
if !errors.Is(err, ErrDivideByZero) { if !errors.Is(err, ErrDivideByZero) {

View File

@ -11,49 +11,115 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
// GetHostsResult wraps the results from the GetHosts RPC method.
type GetHostsResult struct {
Hosts []bootstrap.Host
}
// CreateHostResult wraps the results from the CreateHost RPC method.
type CreateHostResult struct {
JoiningBootstrap JoiningBootstrap
}
// CreateNebulaCertificateResult wraps the results from the
// CreateNebulaCertificate RPC method.
type CreateNebulaCertificateResult struct {
HostNebulaCertificate nebula.Certificate
}
// RPC exposes all RPC methods which are available to be called over the RPC // RPC exposes all RPC methods which are available to be called over the RPC
// interface. // interface.
type RPC struct { type RPC interface {
// CreateNetwork passes through to the Daemon method of the same name.
//
// 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.
// hostName: The name of this first host in the network.
CreateNetwork(
ctx context.Context,
name string,
domain string,
ipNet nebula.IPNet,
hostName nebula.HostName,
) (
struct{}, error,
)
// JoinNetwork passes through to the Daemon method of the same name.
JoinNetwork(
ctx context.Context, req JoiningBootstrap,
) (
struct{}, error,
)
// GetHosts returns all hosts known to the network, sorted by their name.
GetHosts(ctx context.Context) (GetHostsResult, error)
// GetGarageClientParams passes the call through to the Daemon method of the
// same name.
GetGarageClientParams(ctx context.Context) (GarageClientParams, error)
// GetNebulaCAPublicCredentials returns the CAPublicCredentials for the
// network.
GetNebulaCAPublicCredentials(
ctx context.Context,
) (
nebula.CAPublicCredentials, error,
)
// RemoveHost passes the call through to the Daemon method of the same name.
RemoveHost(
ctx context.Context, hostName nebula.HostName,
) (
struct{}, error,
)
// CreateHost passes the call through to the Daemon method of the same name.
CreateHost(
ctx context.Context, hostName nebula.HostName, opts CreateHostOpts,
) (
CreateHostResult, error,
)
// CreateNebulaCertificate passes the call through to the Daemon method of
// the same name.
CreateNebulaCertificate(
ctx context.Context,
hostName nebula.HostName,
hostEncryptingPublicKey nebula.EncryptingPublicKey,
) (
CreateNebulaCertificateResult, error,
)
}
type rpcImpl struct {
daemon Daemon daemon Daemon
} }
// NewRPC initializes and returns an RPC instance. // NewRPC initializes and returns an RPC instance.
func NewRPC(daemon Daemon) *RPC { func NewRPC(daemon Daemon) RPC {
return &RPC{daemon} return &rpcImpl{daemon}
} }
// CreateNetworkRequest contains the arguments to the CreateNetwork RPC method. func (r *rpcImpl) CreateNetwork(
// ctx context.Context,
// All fields are required. name string,
type CreateNetworkRequest struct { domain string,
// Human-readable name of the network. ipNet nebula.IPNet,
Name string hostName nebula.HostName,
// 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
// The name of this first host in the network.
HostName nebula.HostName
}
// CreateNetwork passes through to the Daemon method of the same name.
func (r *RPC) CreateNetwork(
ctx context.Context, req CreateNetworkRequest,
) ( ) (
struct{}, error, struct{}, error,
) { ) {
return struct{}{}, r.daemon.CreateNetwork( return struct{}{}, r.daemon.CreateNetwork(
ctx, req.Name, req.Domain, req.IPNet, req.HostName, ctx, name, domain, ipNet, hostName,
) )
} }
// JoinNetwork passes through to the Daemon method of the same name. func (r *rpcImpl) JoinNetwork(
func (r *RPC) JoinNetwork(
ctx context.Context, req JoiningBootstrap, ctx context.Context, req JoiningBootstrap,
) ( ) (
struct{}, error, struct{}, error,
@ -61,17 +127,7 @@ func (r *RPC) JoinNetwork(
return struct{}{}, r.daemon.JoinNetwork(ctx, req) return struct{}{}, r.daemon.JoinNetwork(ctx, req)
} }
// GetHostsResult wraps the results from the GetHosts RPC method. func (r *rpcImpl) GetHosts(ctx context.Context) (GetHostsResult, error) {
type GetHostsResult struct {
Hosts []bootstrap.Host
}
// GetHosts returns all hosts known to the network, sorted by their name.
func (r *RPC) GetHosts(
ctx context.Context, req struct{},
) (
GetHostsResult, error,
) {
b, err := r.daemon.GetBootstrap(ctx) b, err := r.daemon.GetBootstrap(ctx)
if err != nil { if err != nil {
return GetHostsResult{}, fmt.Errorf("retrieving bootstrap: %w", err) return GetHostsResult{}, fmt.Errorf("retrieving bootstrap: %w", err)
@ -85,19 +141,16 @@ func (r *RPC) GetHosts(
return GetHostsResult{hosts}, nil return GetHostsResult{hosts}, nil
} }
// GetGarageClientParams passes the call through to the Daemon method of the func (r *rpcImpl) GetGarageClientParams(
// same name. ctx context.Context,
func (r *RPC) GetGarageClientParams(
ctx context.Context, req struct{},
) ( ) (
GarageClientParams, error, GarageClientParams, error,
) { ) {
return r.daemon.GetGarageClientParams(ctx) return r.daemon.GetGarageClientParams(ctx)
} }
// GetNebulaCAPublicCredentials returns the CAPublicCredentials for the network. func (r *rpcImpl) GetNebulaCAPublicCredentials(
func (r *RPC) GetNebulaCAPublicCredentials( ctx context.Context,
ctx context.Context, req struct{},
) ( ) (
nebula.CAPublicCredentials, error, nebula.CAPublicCredentials, error,
) { ) {
@ -111,42 +164,20 @@ func (r *RPC) GetNebulaCAPublicCredentials(
return b.CAPublicCredentials, nil return b.CAPublicCredentials, nil
} }
// RemoveHostRequest contains the arguments to the RemoveHost RPC method. func (r *rpcImpl) RemoveHost(
// ctx context.Context, hostName nebula.HostName,
// All fields are required. ) (
type RemoveHostRequest struct { struct{}, error,
HostName nebula.HostName ) {
return struct{}{}, r.daemon.RemoveHost(ctx, hostName)
} }
// RemoveHost passes the call through to the Daemon method of the same name. func (r *rpcImpl) CreateHost(
func (r *RPC) RemoveHost(ctx context.Context, req RemoveHostRequest) (struct{}, error) { ctx context.Context, hostName nebula.HostName, opts CreateHostOpts,
return struct{}{}, r.daemon.RemoveHost(ctx, req.HostName)
}
// CreateHostRequest contains the arguments to the
// CreateHost RPC method.
//
// All fields are required.
type CreateHostRequest struct {
HostName nebula.HostName
Opts CreateHostOpts
}
// CreateHostResult wraps the results from the CreateHost RPC method.
type CreateHostResult struct {
JoiningBootstrap JoiningBootstrap
}
// CreateHost passes the call through to the Daemon method of the
// same name.
func (r *RPC) CreateHost(
ctx context.Context, req CreateHostRequest,
) ( ) (
CreateHostResult, error, CreateHostResult, error,
) { ) {
joiningBootstrap, err := r.daemon.CreateHost( joiningBootstrap, err := r.daemon.CreateHost(ctx, hostName, opts)
ctx, req.HostName, req.Opts,
)
if err != nil { if err != nil {
return CreateHostResult{}, err return CreateHostResult{}, err
} }
@ -154,36 +185,19 @@ func (r *RPC) CreateHost(
return CreateHostResult{JoiningBootstrap: joiningBootstrap}, nil return CreateHostResult{JoiningBootstrap: joiningBootstrap}, nil
} }
// CreateNebulaCertificateRequest contains the arguments to the func (r *rpcImpl) CreateNebulaCertificate(
// CreateNebulaCertificate RPC method. ctx context.Context,
// hostName nebula.HostName,
// All fields are required. hostEncryptingPublicKey nebula.EncryptingPublicKey,
type CreateNebulaCertificateRequest struct {
HostName nebula.HostName
HostEncryptingPublicKey nebula.EncryptingPublicKey
}
// CreateNebulaCertificateResult wraps the results from the
// CreateNebulaCertificate RPC method.
type CreateNebulaCertificateResult struct {
HostNebulaCertifcate nebula.Certificate
}
// CreateNebulaCertificate passes the call through to the Daemon method of the
// same name.
func (r *RPC) CreateNebulaCertificate(
ctx context.Context, req CreateNebulaCertificateRequest,
) ( ) (
CreateNebulaCertificateResult, error, CreateNebulaCertificateResult, error,
) { ) {
cert, err := r.daemon.CreateNebulaCertificate( cert, err := r.daemon.CreateNebulaCertificate(
ctx, req.HostName, req.HostEncryptingPublicKey, ctx, hostName, hostEncryptingPublicKey,
) )
if err != nil { if err != nil {
return CreateNebulaCertificateResult{}, err return CreateNebulaCertificateResult{}, err
} }
return CreateNebulaCertificateResult{ return CreateNebulaCertificateResult{HostNebulaCertificate: cert}, nil
HostNebulaCertifcate: cert,
}, nil
} }

View File

@ -11,6 +11,11 @@ var hostNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
// lowercase letters, numbers, and hyphens, and must start with a letter. // lowercase letters, numbers, and hyphens, and must start with a letter.
type HostName string type HostName string
// MarshalText casts the HostName to a byte string and returns it.
func (h HostName) MarshalText() ([]byte, error) {
return []byte(h), nil
}
// UnmarshalText parses and validates a HostName from a text string. // UnmarshalText parses and validates a HostName from a text string.
func (h *HostName) UnmarshalText(b []byte) error { func (h *HostName) UnmarshalText(b []byte) error {
if !hostNameRegexp.Match(b) { if !hostNameRegexp.Match(b) {

17
nix/gowrap.nix Normal file
View File

@ -0,0 +1,17 @@
{
buildGoModule,
fetchFromGitHub,
}: let
version = "1.4.0";
in buildGoModule {
pname = "gowrap";
inherit version;
src = fetchFromGitHub {
owner = "hexdigest";
repo = "gowrap";
rev = "v${version}";
hash = "sha256-eEaUANLnxRGfVbhOTwJV+R9iEWMObg0lHqmwO3AYuIk=";
};
vendorHash = "sha256-xIOyXXt8WSGQYIvIam+0e25VNI7awYEEYZBe7trC6zQ=";
subPackages = [ "cmd/gowrap" ];
}

View File

@ -14,6 +14,7 @@ in pkgs.mkShell {
buildInputs = [ buildInputs = [
pkgs.go pkgs.go
pkgs.golangci-lint pkgs.golangci-lint
(pkgs.callPackage ./nix/gowrap.nix {})
]; ];
shellHook = '' shellHook = ''
true # placeholder true # placeholder