blob: 497a2bd81f9ce8acd6f61b7781b6e44745dacc7a [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package wakeuplib implements utilities for wakeup server implementation.
package wakeuplib
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strings"
"time"
"golang.org/x/crypto/nacl/secretbox"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/naming"
"v.io/v23/options"
"v.io/v23/rpc"
"v.io/v23/security"
"v.io/v23/security/access"
"v.io/v23/services/mounttable"
"v.io/v23/services/wakeup"
"v.io/v23/verror"
"v.io/x/ref/services/mounttable/mounttablelib"
)
const (
pkgPath = "v.io/x/ref/services/wakeup/wakeuplib"
)
var (
errInvalidName = verror.Register(pkgPath+".errInvalidName", verror.NoRetry, "{1:}{2:} invalid name format {3}; can only be \"mounts/<token>/<svc_name>/{client,server}\" or \"server\" {:_}")
)
// StartServers starts the wakeup server and an associated mounttable,
// returning a function that should be used to stop the servers or an
// error if the servers couldn't be started.
//
// The mounttable is mounted at the provided mountName. The wakeup
// server is mounted under <mountName>/server.
//
// Provided mtPersistDir directory is used for persisting mounttable
// entries.
//
// Provided sharedKey is used for obscuring wakeup tokens.
//
// Finally, provided wake function is used for waking up the remote
// server using an associated wakeup token and the service name
// component of the mount name.
func StartServers(ctx *context.T, mountName, mtPersistDir string, sharedKey [32]byte, wake func(*context.T, string, string) error) (func(), error) {
var stopFuncs []func()
ctx, cancel := context.WithCancel(ctx)
stop := func() {
cancel()
for i := len(stopFuncs) - 1; i >= 0; i-- {
stopFuncs[i]()
}
}
// Root the mount name.
if !naming.Rooted(mountName) {
nsRoots := v23.GetNamespace(ctx).Roots()
if len(nsRoots) == 0 {
return nil, fmt.Errorf("Namespace roots not configured")
}
mountName = naming.Join(nsRoots[0], mountName)
}
// Start the mounttable server.
mtPermsFile, err := createMTPermsFile(ctx)
if err != nil {
return nil, fmt.Errorf("Couldn't create mounttable permissions file: %v", mtPermsFile)
}
mtDispatcher, err := mounttablelib.NewMountTableDispatcher(ctx, mtPermsFile, mtPersistDir, "debug")
if err != nil {
return nil, err
}
mt := &wakeupMT{
mtDispatcher: mtDispatcher,
sharedKey: sharedKey,
wakeup: wake,
}
ctx, mtServer, err := v23.WithNewDispatchingServer(ctx, mountName, mt, options.ServesMountTable(true))
if err != nil {
return nil, err
}
stopFuncs = append(stopFuncs, func() {
<-mtServer.Closed()
})
if err := waitServerMounted(mtServer, mountName, 30*time.Second); err != nil {
stop()
return nil, err
}
// Start the wakeup server.
w := &wakeupServer{
wakeupMountPrefix: naming.Join(mountName, "mounts"),
sharedKey: [32]byte(sharedKey),
}
ctx, wakeupServer, err := v23.WithNewServer(ctx, naming.Join(mountName, "server"), wakeup.WakeUpServer(w), security.AllowEveryone())
if err != nil {
stop()
return nil, err
}
stopFuncs = append(stopFuncs, func() {
<-wakeupServer.Closed()
})
if err := waitServerMounted(wakeupServer, naming.Join(mountName, "server"), 30*time.Second); err != nil {
stop()
return nil, err
}
return stop, nil
}
type wakeupServer struct {
wakeupMountPrefix string
sharedKey [32]byte
}
func (w *wakeupServer) Register(ctx *context.T, call rpc.ServerCall, token string) (string, error) {
if len(token) == 0 {
return "", fmt.Errorf("Token cannot be empty.")
}
// Convert the token into an obscured name.
name, err := encodeToken(token, w.sharedKey)
if err != nil {
return "", err
}
mtName := naming.Join(w.wakeupMountPrefix, name)
// Create mounttable entry for the name and set permissions on it.
names, _ := security.RemoteBlessingNames(ctx, call.Security())
if err := v23.GetNamespace(ctx).SetPermissions(ctx, mtName, mtPerms(names...), ""); err != nil {
return "", err
}
return mtName, nil
}
// Produces: base64(<rand_nonce>secretbox(token))
func encodeToken(token string, sharedKey [32]byte) (string, error) {
// Generate random nonce.
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
return "", err
}
s := secretbox.Seal(nil, []byte(token), &nonce, &sharedKey)
return base64.URLEncoding.EncodeToString(append(nonce[:], s...)), nil
}
func decodeToken(enc string, sharedKey [32]byte) (string, error) {
data, err := base64.URLEncoding.DecodeString(enc)
if err != nil {
return "", fmt.Errorf("Couldn't base64 decode string %q: %v", enc, err)
}
var nonce [24]byte
if n := copy(nonce[:], data); n < 24 {
return "", fmt.Errorf("Couldn't decode token: encoded length must be at least 24 bytes: %v.", enc)
}
b, ok := secretbox.Open(nil, data[24:], &nonce, &sharedKey)
if !ok {
return "", fmt.Errorf("Couldn't decrypt token.")
}
return string(b), nil
}
type wakeupMT struct {
wakeupMountPrefix string
mtDispatcher rpc.Dispatcher
sharedKey [32]byte
wakeup func(*context.T, string, string) error
}
// Lookup implements rpc.Dispatcher.Lookup.
// We allow a very restricted set of names to be used, in particular:
// - "server", used by the clients to talk to the wakeup service,
// - "mounts/<token_sig>", used by the wakeup service to set permissions
// - "mounts/<token_sig>/<svc_name>/server/*", used by the wakeable service to mount itself
// - "mounts/<token_sig>/<svc_name>/client/*", used by the by the clients to resolve the
// wakeup-registered services
func (w *wakeupMT) Lookup(ctx *context.T, name string) (interface{}, security.Authorizer, error) {
if len(name) == 0 || name == "server" {
// Wakeup service mounting itself or a client lookup for wakeup service.
return w.mtDispatcher.Lookup(ctx, name)
}
if !strings.HasPrefix(name, "mounts/") {
return nil, nil, verror.New(errInvalidName, ctx, name)
}
parts := strings.Split(name, "/")
if len(parts) < 3 {
// Wakeup service setting permissions.
return w.mtDispatcher.Lookup(ctx, name)
}
if len(parts) < 4 {
return nil, nil, verror.New(errInvalidName, ctx, name)
}
// Either a wakeable service mounting itself or client resolving a name (with a wakeup).
if parts[3] != "client" && parts[3] != "server" {
return nil, nil, verror.New(errInvalidName, ctx, name)
}
// Strip away the client/server part of the name: it's used only as a signal to us
// whether the client or the server is accessing the name (the latter doesn't trigger
// wakeups).
isServer := parts[3] == "server"
parts = append(parts[:3], parts[4:]...)
mountedName := naming.Join(parts...)
if isServer {
return w.mtDispatcher.Lookup(ctx, mountedName)
}
// Extract token from the name.
token, err := decodeToken(parts[1], w.sharedKey)
if err != nil {
return nil, nil, err
}
serviceName := parts[2]
// Get the the mounttable server.
server, authorizer, err := w.mtDispatcher.Lookup(ctx, mountedName)
if err != nil {
return nil, nil, err
}
mtServer, ok := server.(mounttable.MountTableServerMethods)
if !ok {
return nil, nil, fmt.Errorf("invalid mounttable server of type %T", server)
}
wakeupMTServer := &wakeupMTContext{
name: name,
token: token,
serviceName: serviceName,
wakeup: w.wakeup,
mtServer: mtServer,
}
return mounttable.MountTableServer(wakeupMTServer), authorizer, nil
}
type wakeupMTContext struct {
name, token, serviceName string
wakeup func(*context.T, string, string) error
mtServer mounttable.MountTableServerMethods
}
func (w *wakeupMTContext) Mount(ctx *context.T, call rpc.ServerCall, server string, ttl uint32, flags naming.MountFlag) error {
return w.mtServer.Mount(ctx, call, server, ttl, flags)
}
func (w *wakeupMTContext) Unmount(ctx *context.T, call rpc.ServerCall, server string) error {
return w.mtServer.Unmount(ctx, call, server)
}
func (w *wakeupMTContext) Delete(ctx *context.T, call rpc.ServerCall, deleteSubtree bool) error {
return w.mtServer.Delete(ctx, call, deleteSubtree)
}
func (w *wakeupMTContext) ResolveStep(ctx *context.T, call rpc.ServerCall) (naming.MountEntry, error) {
if err := w.wakeup(ctx, w.token, w.serviceName); err != nil {
ctx.Errorf("couldn't wakeup remote server under name %s: %v", w.name, err)
}
return w.mtServer.ResolveStep(ctx, call)
}
func (w *wakeupMTContext) SetPermissions(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error {
return w.mtServer.SetPermissions(ctx, call, perms, version)
}
func (w *wakeupMTContext) GetPermissions(ctx *context.T, call rpc.ServerCall) (perms access.Permissions, version string, _ error) {
return w.mtServer.GetPermissions(ctx, call)
}
func mtPerms(blessingNames ...string) access.Permissions {
p := make(access.Permissions)
for _, name := range blessingNames {
p = p.Add(security.BlessingPattern(name), "Admin", "Mount")
}
p = p.Add(security.BlessingPattern("..."), "Resolve")
return p.Normalize()
}
func createMTPermsFile(ctx *context.T) (string, error) {
data := make(map[string]access.Permissions)
b, _ := v23.GetPrincipal(ctx).BlessingStore().Default()
data[""] = mtPerms(security.BlessingNames(v23.GetPrincipal(ctx), b)...)
mtPermsFile, err := ioutil.TempFile("", "perms")
if err != nil {
return "", fmt.Errorf("Error creating permissions file: %v", err)
}
if err := json.NewEncoder(mtPermsFile).Encode(data); err != nil {
return "", err
}
if err := mtPermsFile.Sync(); err != nil {
return "", err
}
return mtPermsFile.Name(), nil
}
func waitServerMounted(server rpc.Server, name string, timeout time.Duration) error {
t := time.After(timeout)
for true {
status := server.Status()
mounted := true
for _, pub := range status.PublisherStatus {
if pub.LastState != pub.DesiredState {
mounted = false
break
}
}
if mounted {
return nil
}
select {
case <-status.Dirty:
case <-t:
return fmt.Errorf("Couldn't mount at name: %s", name)
}
}
return nil
}