multi: implement lit-autopilot rpc server

This commit is contained in:
Elle Mouton 2022-07-08 14:33:00 +02:00
parent f9ffc0cdef
commit 3b67325360
No known key found for this signature in database
GPG key ID: D7D916376026F177
5 changed files with 613 additions and 23 deletions

View file

@ -153,6 +153,14 @@ var (
litConn := litrpc.NewAccountsClient(c)
return litConn.ListAccounts(ctx, &litrpc.ListAccountsRequest{})
}
litAutopilotRequestFn = func(ctx context.Context,
c grpc.ClientConnInterface) (proto.Message, error) {
litConn := litrpc.NewAutopilotClient(c)
return litConn.ListAutopilotFeatures(
ctx, &litrpc.ListAutopilotFeaturesRequest{},
)
}
litMacaroonFn = func(cfg *LitNodeConfig) string {
return cfg.LitMacPath
}
@ -232,6 +240,13 @@ var (
successPattern: "\"accounts\":[]",
allowedThroughLNC: false,
grpcWebURI: "/litrpc.Accounts/ListAccounts",
}, {
name: "litrpc-autopilot",
macaroonFn: litMacaroonFn,
requestFn: litAutopilotRequestFn,
successPattern: "\"features\":{",
allowedThroughLNC: true,
grpcWebURI: "/litrpc.Autopilot/ListAutopilotFeatures",
}}
// customURIs is a map of endpoint URIs that we want to allow via a

View file

@ -62,10 +62,26 @@ var (
Entity: "account",
Action: "write",
}},
"/litrpc.Lit/ListActions": {{
"/litrpc.Firewall/ListActions": {{
Entity: "actions",
Action: "read",
}},
"/litrpc.Autopilot/ListAutopilotFeatures": {{
Entity: "autopilot",
Action: "read",
}},
"/litrpc.Autopilot/AddAutopilotSession": {{
Entity: "autopilot",
Action: "write",
}},
"/litrpc.Autopilot/ListAutopilotSessions": {{
Entity: "autopilot",
Action: "read",
}},
"/litrpc.Autopilot/RevokeAutopilotSession": {{
Entity: "autopilot",
Action: "write",
}},
}
// whiteListedLNDMethods is a map of all lnd RPC methods that don't

View file

@ -15,12 +15,12 @@ import (
type Type uint8
const (
TypeMacaroonReadonly Type = 0
TypeMacaroonAdmin Type = 1
TypeMacaroonCustom Type = 2
TypeUIPassword Type = 3
typeReservedForFutureUse Type = 4
TypeMacaroonAccount Type = 5
TypeMacaroonReadonly Type = 0
TypeMacaroonAdmin Type = 1
TypeMacaroonCustom Type = 2
TypeUIPassword Type = 3
TypeAutopilot Type = 4
TypeMacaroonAccount Type = 5
)
// State represents the state of a session.

View file

@ -2,6 +2,8 @@ package terminal
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
@ -10,9 +12,12 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightninglabs/lightning-node-connect/mailbox"
"github.com/lightninglabs/lightning-terminal/accounts"
"github.com/lightninglabs/lightning-terminal/autopilotserver"
"github.com/lightninglabs/lightning-terminal/firewall"
"github.com/lightninglabs/lightning-terminal/firewalldb"
"github.com/lightninglabs/lightning-terminal/litrpc"
"github.com/lightninglabs/lightning-terminal/perms"
"github.com/lightninglabs/lightning-terminal/rules"
"github.com/lightninglabs/lightning-terminal/session"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
@ -33,6 +38,7 @@ const readOnlyAction = "***readonly***"
type sessionRpcServer struct {
litrpc.UnimplementedSessionsServer
litrpc.UnimplementedFirewallServer
litrpc.UnimplementedAutopilotServer
cfg *sessionRpcServerConfig
db *session.DB
@ -54,6 +60,9 @@ type sessionRpcServerConfig struct {
firstConnectionDeadline time.Duration
permMgr *perms.Manager
actionsDB *firewalldb.DB
autopilot autopilotserver.Autopilot
ruleMgrs rules.ManagerSet
privMap firewalldb.NewPrivacyMapDB
}
// newSessionRPCServer creates a new sessionRpcServer using the passed config.
@ -96,10 +105,68 @@ func (s *sessionRpcServer) start() error {
if err != nil {
return fmt.Errorf("error listing sessions: %v", err)
}
for _, sess := range sessions {
key := sess.LocalPublicKey.SerializeCompressed()
if sess.Type == session.TypeAutopilot {
// We only start the autopilot sessions if the autopilot
// client has been enabled.
if s.cfg.autopilot == nil {
continue
}
// Do a sanity check to ensure that we have the static
// remote pub key stored for this session. This should
// never not be the case.
if sess.RemotePublicKey == nil {
log.Errorf("no static remote key found for "+
"autopilot session %x", key)
continue
}
if sess.State != session.StateInUse &&
sess.State != session.StateCreated {
continue
}
if sess.Expiry.Before(time.Now()) {
continue
}
ctx := context.Background()
ctxc, cancel := context.WithTimeout(
ctx, defaultConnectTimeout,
)
// Register the session with the autopilot client.
perm, err := s.cfg.autopilot.ActivateSession(
ctxc, sess.LocalPublicKey,
)
cancel()
if err != nil {
log.Errorf("error activating autopilot "+
"session (%x) with the client", key,
err)
if perm {
err := s.db.RevokeSession(
sess.LocalPublicKey,
)
if err != nil {
log.Errorf("error revoking "+
"session: %v", err)
}
continue
}
}
}
if err := s.resumeSession(sess); err != nil {
log.Errorf("error resuming session (%x): %v",
sess.LocalPublicKey.SerializeCompressed(), err)
log.Errorf("error resuming session (%x): %v", key, err)
}
}
@ -232,8 +299,9 @@ func (s *sessionRpcServer) AddSession(_ context.Context,
// No other types are currently supported.
default:
return nil, fmt.Errorf("invalid session type, only admin, " +
"readonly, account and custom macaroon types " +
"supported in LiT")
"readonly, custom and account macaroon types supported in " +
"LiT. Autopilot sessions must be added using " +
"AddAutoPilotSession method")
}
// Collect the de-duped permissions.
@ -263,7 +331,7 @@ func (s *sessionRpcServer) AddSession(_ context.Context,
return nil, fmt.Errorf("error starting session: %v", err)
}
rpcSession, err := marshalRPCSession(sess)
rpcSession, err := s.marshalRPCSession(sess)
if err != nil {
return nil, fmt.Errorf("error marshaling session: %v", err)
}
@ -325,11 +393,14 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error {
// For custom session types, we use the caveats and permissions that
// were persisted on session creation.
case session.TypeMacaroonCustom:
if sess.MacaroonRecipe != nil {
permissions = sess.MacaroonRecipe.Permissions
case session.TypeMacaroonCustom, session.TypeAutopilot:
if sess.MacaroonRecipe == nil {
break
}
permissions = sess.MacaroonRecipe.Permissions
caveats = append(caveats, sess.MacaroonRecipe.Caveats...)
// No other types are currently supported.
default:
log.Debugf("Not resuming session %x with type %d", pubKeyBytes,
@ -436,16 +507,24 @@ func (s *sessionRpcServer) resumeSession(sess *session.Session) error {
pubKeyBytes)
}
if s.cfg.autopilot != nil {
ctx := context.Background()
ctxc, cancel := context.WithTimeout(
ctx, defaultConnectTimeout,
)
s.cfg.autopilot.SessionRevoked(ctxc, pubKey)
cancel()
}
err = s.sessionServer.StopSession(pubKey)
if err != nil {
log.Debugf("Error stopping session: "+
"%v", err)
log.Debugf("Error stopping session: %v", err)
}
err = s.db.RevokeSession(pubKey)
if err != nil {
log.Debugf("error revoking session: "+
"%v", err)
log.Debugf("error revoking session: %v", err)
}
}()
@ -465,7 +544,7 @@ func (s *sessionRpcServer) ListSessions(_ context.Context,
Sessions: make([]*litrpc.Session, len(sessions)),
}
for idx, sess := range sessions {
response.Sessions[idx], err = marshalRPCSession(sess)
response.Sessions[idx], err = s.marshalRPCSession(sess)
if err != nil {
return nil, fmt.Errorf("error marshaling session: %v",
err)
@ -477,7 +556,7 @@ func (s *sessionRpcServer) ListSessions(_ context.Context,
// RevokeSession revokes a single session and also stops it if it is currently
// active.
func (s *sessionRpcServer) RevokeSession(_ context.Context,
func (s *sessionRpcServer) RevokeSession(ctx context.Context,
req *litrpc.RevokeSessionRequest) (*litrpc.RevokeSessionResponse, error) {
pubKey, err := btcec.ParsePubKey(req.LocalPublicKey)
@ -489,6 +568,10 @@ func (s *sessionRpcServer) RevokeSession(_ context.Context,
return nil, fmt.Errorf("error revoking session: %v", err)
}
if s.cfg.autopilot != nil {
s.cfg.autopilot.SessionRevoked(ctx, pubKey)
}
// If the session expired already it might not be running anymore. So we
// only log possible errors here.
if err := s.sessionServer.StopSession(pubKey); err != nil {
@ -604,7 +687,6 @@ func (s *sessionRpcServer) ListActions(_ context.Context,
return nil, err
}
}
resp := make([]*litrpc.Action, len(actions))
for i, a := range actions {
state, err := marshalActionState(a.State)
@ -634,8 +716,438 @@ func (s *sessionRpcServer) ListActions(_ context.Context,
}, nil
}
// ListAutopilotFeatures fetches all the features supported by the autopilot
// server along with the rules that we need to support in order to subscribe
// to those features.
func (s *sessionRpcServer) ListAutopilotFeatures(ctx context.Context,
_ *litrpc.ListAutopilotFeaturesRequest) (
*litrpc.ListAutopilotFeaturesResponse, error) {
fs, err := s.cfg.autopilot.ListFeatures(ctx)
if err != nil {
return nil, err
}
features := make(map[string]*litrpc.Feature, len(fs))
for i, f := range fs {
rules, upgrade, err := convertRules(s.cfg.ruleMgrs, f.Rules)
if err != nil {
return nil, err
}
features[i] = &litrpc.Feature{
Name: f.Name,
Description: f.Description,
Rules: rules,
PermissionsList: marshalPerms(f.Permissions),
RequiresUpgrade: upgrade,
}
}
return &litrpc.ListAutopilotFeaturesResponse{
Features: features,
}, nil
}
// AddAutopilotSession creates a new LNC session and attempts to register it
// with the Autopilot server.
func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context,
req *litrpc.AddAutopilotSessionRequest) (
*litrpc.AddAutopilotSessionResponse, error) {
if len(req.Features) == 0 {
return nil, fmt.Errorf("must include at least one feature")
}
expiry := time.Unix(int64(req.ExpiryTimestampSeconds), 0)
if time.Now().After(expiry) {
return nil, fmt.Errorf("expiry must be in the future")
}
privacy := !req.NoPrivacyMapper
privacyMapPairs := make(map[string]string)
// First need to fetch all the perms that need to be baked into this
// mac based on the features.
allFeatures, err := s.cfg.autopilot.ListFeatures(ctx)
if err != nil {
return nil, err
}
// Create lookup map of all the features that autopilot server offers.
autopilotFeatureMap := make(map[string]*autopilotserver.Feature)
for _, f := range allFeatures {
autopilotFeatureMap[f.Name] = f
}
// allRules represents all the rules that our firewall knows about.
allRules := s.cfg.ruleMgrs.GetAllRules()
// Check that each requested feature is a valid autopilot feature and
// that the necessary rules for the feature have been specified.
featureRules := make(map[string]map[string]string, len(req.Features))
for f, rs := range req.Features {
// Check that the features is known by the autopilot server.
autopilotFeature, ok := autopilotFeatureMap[f]
if !ok {
return nil, fmt.Errorf("%s is not a features "+
"provided by the Autopilot server", f)
}
// reqRules is the rules specified in the request.
var reqRules []rules.Values
if rs.Rules != nil {
reqRules = make([]rules.Values, 0, len(rs.Rules.Rules))
for ruleName, rule := range rs.Rules.Rules {
v, err := s.cfg.ruleMgrs.UnmarshalRuleValues(
ruleName, rule,
)
if err != nil {
return nil, err
}
if privacy {
var privMapPairs map[string]string
v, privMapPairs, err = v.RealToPseudo()
if err != nil {
return nil, err
}
for k, v := range privMapPairs {
privacyMapPairs[k] = v
}
}
reqRules = append(reqRules, v)
}
}
// Create a lookup map for the rules specified in this feature.
// Also check that each of the rules in the request is one known
// to us.
frs := make(map[string]rules.Values)
for _, r := range reqRules {
frs[r.RuleName()] = r
_, ok := allRules[r.RuleName()]
if !ok {
return nil, fmt.Errorf("%s is not a known rule",
r.RuleName())
}
}
// For each of the specified rules, we check that the values
// set for those rules are sane given the bounds provided by
// the autopilot server for the given feature.
for _, r := range reqRules {
ruleName := r.RuleName()
autopilotSpecs, ok := autopilotFeature.Rules[ruleName]
if !ok {
return nil, fmt.Errorf("autopilot did not "+
"specify %s as a rule for feature %s",
ruleName, autopilotFeature.Name)
}
min, err := s.cfg.ruleMgrs.InitRuleValues(
ruleName, autopilotSpecs.MinVal,
)
if err != nil {
return nil, err
}
max, err := s.cfg.ruleMgrs.InitRuleValues(
ruleName, autopilotSpecs.MaxVal,
)
if err != nil {
return nil, err
}
if err = r.VerifySane(min, max); err != nil {
return nil, fmt.Errorf("rule value for %s "+
"not valid for feature %s. Expected "+
"rule value between %s and %s. Got "+
"%s. %v", ruleName,
autopilotFeature.Name, min, max, r, err)
}
}
// If the request did not contain specific values for a rule,
// the default Autopilot rule value is used.
finalRules := make(
[]rules.Values, 0, len(autopilotFeature.Rules),
)
for name, values := range autopilotFeature.Rules {
if r, ok := frs[name]; ok {
finalRules = append(finalRules, r)
continue
}
defaults, err := s.cfg.ruleMgrs.InitRuleValues(
name, values.Default,
)
if err != nil {
return nil, err
}
finalRules = append(finalRules, defaults)
}
featureRules[f], err = marshalRulesToStringMap(finalRules)
if err != nil {
return nil, err
}
}
interceptRules := &firewall.InterceptRules{
FeatureRules: featureRules,
}
// Gather all the permissions we need to add to the macaroon given the
// feature list.
var dedupedPerms = make(map[string]map[string]bool)
for name, feature := range autopilotFeatureMap {
if _, ok := req.Features[name]; !ok {
continue
}
for _, ops := range feature.Permissions {
for _, op := range ops {
if dedupedPerms[op.Entity] == nil {
dedupedPerms[op.Entity] = make(
map[string]bool,
)
}
dedupedPerms[op.Entity][op.Action] = true
}
}
}
var perms []bakery.Op
for entity, actions := range dedupedPerms {
for action := range actions {
perms = append(perms, bakery.Op{
Entity: entity,
Action: action,
})
}
}
rulesCaveatStr, err := firewall.RulesToCaveat(interceptRules)
if err != nil {
return nil, err
}
featureConfig := make(map[string][]byte, len(req.Features))
for name, f := range req.Features {
featureConfig[name] = f.Config
}
caveats := []macaroon.Caveat{{Id: []byte(rulesCaveatStr)}}
if privacy {
caveats = append(caveats, firewall.MetaPrivacyCaveat)
}
sess, err := session.NewSession(
req.Label, session.TypeAutopilot, expiry, req.MailboxServerAddr,
req.DevServer, perms, caveats, featureConfig, privacy,
)
if err != nil {
return nil, fmt.Errorf("error creating new session: %v", err)
}
// Register all the privacy map pairs for this session ID.
privDB := s.cfg.privMap(sess.ID)
err = privDB.Update(func(tx firewalldb.PrivacyMapTx) error {
for r, p := range privacyMapPairs {
err := tx.NewPair(r, p)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
// Attempt to register the session with the Autopilot server.
remoteKey, err := s.cfg.autopilot.RegisterSession(
ctx, sess.LocalPublicKey, sess.ServerAddr, sess.DevServer,
featureConfig,
)
if err != nil {
return nil, fmt.Errorf("error registering session with "+
"autopilot server: %v", err)
}
// We only persist this session if we successfully retrieved the
// autopilot's static key.
sess.RemotePublicKey = remoteKey
if err := s.db.StoreSession(sess); err != nil {
return nil, fmt.Errorf("error storing session: %v", err)
}
if err := s.resumeSession(sess); err != nil {
return nil, fmt.Errorf("error starting session: %v", err)
}
rpcSession, err := s.marshalRPCSession(sess)
if err != nil {
return nil, fmt.Errorf("error marshaling session: %v", err)
}
return &litrpc.AddAutopilotSessionResponse{
Session: rpcSession,
}, nil
}
// ListAutopilotSessions fetches and returns all the sessions from the DB that
// are of type TypeAutopilot.
func (s *sessionRpcServer) ListAutopilotSessions(_ context.Context,
_ *litrpc.ListAutopilotSessionsRequest) (
*litrpc.ListAutopilotSessionsResponse, error) {
sessions, err := s.db.ListSessions(func(s *session.Session) bool {
return s.Type == session.TypeAutopilot
})
if err != nil {
return nil, fmt.Errorf("error fetching sessions: %v", err)
}
response := &litrpc.ListAutopilotSessionsResponse{
Sessions: make([]*litrpc.Session, len(sessions)),
}
for idx, sess := range sessions {
response.Sessions[idx], err = s.marshalRPCSession(sess)
if err != nil {
return nil, fmt.Errorf("error marshaling session: %v",
err)
}
}
return response, nil
}
// RevokeAutopilotSession revokes an autopilot session.
func (s *sessionRpcServer) RevokeAutopilotSession(ctx context.Context,
req *litrpc.RevokeAutopilotSessionRequest) (
*litrpc.RevokeAutopilotSessionResponse, error) {
pubKey, err := btcec.ParsePubKey(req.LocalPublicKey)
if err != nil {
return nil, fmt.Errorf("error parsing public key: %v", err)
}
sess, err := s.db.GetSession(pubKey)
if err != nil {
return nil, err
}
if sess.Type != session.TypeAutopilot {
return nil, session.ErrSessionNotFound
}
_, err = s.RevokeSession(
ctx, &litrpc.RevokeSessionRequest{
LocalPublicKey: req.LocalPublicKey,
},
)
if err != nil {
return nil, err
}
return &litrpc.RevokeAutopilotSessionResponse{}, nil
}
func marshalRulesToStringMap(rs []rules.Values) (map[string]string, error) {
res := make(map[string]string, len(rs))
for _, r := range rs {
b, err := json.Marshal(r)
if err != nil {
return nil, err
}
res[r.RuleName()] = string(b)
}
return res, nil
}
// marshalPerms attempts to convert a set of permissions into their RPC
// counterpart. If the list includes a rule that LiT does not know about, a nil
// entry is included in the returned map. A bool is returned that indicates if
// an upgrade is needed in order to enforce an unknown rule.
func convertRules(ruleMgr rules.ManagerSet,
ruleList map[string]*autopilotserver.RuleValues) (
map[string]*litrpc.RuleValues, bool, error) {
var (
upgrade bool
res = make(
map[string]*litrpc.RuleValues, len(ruleList),
)
knownRules = ruleMgr.GetAllRules()
)
for name, rule := range ruleList {
known := true
if !knownRules[name] {
upgrade = true
known = false
}
defaultVals, err := ruleMgr.InitRuleValues(name, rule.Default)
if err != nil {
return nil, false, err
}
minVals, err := ruleMgr.InitRuleValues(name, rule.MinVal)
if err != nil {
return nil, false, err
}
maxVals, err := ruleMgr.InitRuleValues(name, rule.MaxVal)
if err != nil {
return nil, false, err
}
res[name] = &litrpc.RuleValues{
Known: known,
Defaults: defaultVals.ToProto(),
MinValue: minVals.ToProto(),
MaxValue: maxVals.ToProto(),
}
}
return res, upgrade, nil
}
// marshalPerms converts a set of permissions into their RPC counterpart.
func marshalPerms(perms map[string][]bakery.Op) []*litrpc.Permissions {
var res []*litrpc.Permissions
for method, ops := range perms {
rpcOps := make([]*litrpc.MacaroonPermission, len(ops))
for i, op := range ops {
rpcOps[i] = &litrpc.MacaroonPermission{
Entity: op.Entity,
Action: op.Action,
}
}
res = append(res, &litrpc.Permissions{
Method: method,
Operations: rpcOps,
})
}
return res
}
// marshalRPCSession converts a session into its RPC counterpart.
func marshalRPCSession(sess *session.Session) (*litrpc.Session, error) {
func (s *sessionRpcServer) marshalRPCSession(sess *session.Session) (
*litrpc.Session, error) {
rpcState, err := marshalRPCState(sess.State)
if err != nil {
return nil, err
@ -663,6 +1175,40 @@ func marshalRPCSession(sess *session.Session) (*litrpc.Session, error) {
revokedAt = uint64(sess.RevokedAt.Unix())
}
featureInfo := make(map[string]*litrpc.RulesMap)
if sess.MacaroonRecipe != nil {
for _, cav := range sess.MacaroonRecipe.Caveats {
info, err := firewall.ParseRuleCaveat(string(cav.Id))
if errors.Is(err, firewall.ErrNoRulesCaveat) {
continue
} else if err != nil {
return nil, err
}
for feature, rules := range info.FeatureRules {
ruleMap := make(map[string]*litrpc.RuleValue)
for name, rule := range rules {
val, err := s.cfg.ruleMgrs.InitRuleValues(name, []byte(rule))
if err != nil {
return nil, err
}
if sess.WithPrivacyMapper {
db := s.cfg.privMap(sess.ID)
val, err = val.PseudoToReal(db)
if err != nil {
return nil, err
}
}
ruleMap[name] = val.ToProto()
}
featureInfo[feature] = &litrpc.RulesMap{Rules: ruleMap}
}
}
}
return &litrpc.Session{
Id: sess.ID[:],
Label: sess.Label,
@ -678,6 +1224,7 @@ func marshalRPCSession(sess *session.Session) (*litrpc.Session, error) {
CreatedAt: uint64(sess.CreatedAt.Unix()),
RevokedAt: revokedAt,
MacaroonRecipe: macRecipe,
AutopilotFeatureInfo: featureInfo,
}, nil
}
@ -747,6 +1294,9 @@ func marshalRPCType(typ session.Type) (litrpc.SessionType, error) {
case session.TypeMacaroonAccount:
return litrpc.SessionType_TYPE_MACAROON_ACCOUNT, nil
case session.TypeAutopilot:
return litrpc.SessionType_TYPE_AUTOPILOT, nil
default:
return 0, fmt.Errorf("unknown type <%d>", typ)
}
@ -770,6 +1320,9 @@ func unmarshalRPCType(typ litrpc.SessionType) (session.Type, error) {
case litrpc.SessionType_TYPE_MACAROON_ACCOUNT:
return session.TypeMacaroonAccount, nil
case litrpc.SessionType_TYPE_AUTOPILOT:
return session.TypeAutopilot, nil
default:
return 0, fmt.Errorf("unknown type <%d>", typ)
}

View file

@ -321,6 +321,8 @@ func (g *LightningTerminal) Run() error {
firstConnectionDeadline: g.cfg.FirstLNCConnDeadline,
permMgr: g.permsMgr,
actionsDB: g.firewallDB,
autopilot: g.autopilotClient,
ruleMgrs: g.ruleMgrs,
})
if err != nil {
return fmt.Errorf("could not create new session rpc "+
@ -818,6 +820,10 @@ func (g *LightningTerminal) registerSubDaemonGrpcServers(server *grpc.Server,
}
litrpc.RegisterFirewallServer(server, g.sessionRpcServer)
if !g.cfg.Autopilot.Disable {
litrpc.RegisterAutopilotServer(server, g.sessionRpcServer)
}
}
// RegisterRestSubserver is a callback on the lnd.SubserverConfig struct that is