package firewall import ( "context" "fmt" "strings" "sync" "github.com/lightninglabs/lightning-terminal/firewalldb" litmac "github.com/lightninglabs/lightning-terminal/macaroons" mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/macaroons" ) const ( // RequestLoggerName is the name of the RequestLogger interceptor. RequestLoggerName = "lit-macaroon-firewall-logger" ) var ( // uriSkipList is a map of URIs that we don't want to log or persist // actions for. uriSkipList = map[string]bool{ // We skip the CheckMacaroonPermissions uri since this method is // called each time a call to a non-Litd endpoint needs to be // validated and persisting the macaroon each time bloats the // DB. "/lnrpc.Lightning/CheckMacaroonPermissions": true, } // A compile-time assertion that RuleEnforcer is a // rpcmiddleware.RequestInterceptor. _ mid.RequestInterceptor = (*RequestLogger)(nil) ) type RequestLoggerLevel string const ( RequestLoggerLevelInterceptor = "interceptor" RequestLoggerLevelAll = "all" RequestLoggerLevelFull = "full" ) // RequestLogger is a RequestInterceptor that just logs incoming RPC requests. type RequestLogger struct { actionsDB firewalldb.ActionsWriteDB shouldLogAction func(ri *RequestInfo) (bool, bool) // reqIDToAction is a map from request ID to an ActionLocator that can // be used to find the corresponding action. This is used so that // requests and responses can be easily linked. The mu mutex must be // used when accessing this map. reqIDToAction map[uint64]firewalldb.ActionLocator mu sync.Mutex } // NewRequestLogger creates a new RequestLogger. func NewRequestLogger(cfg *RequestLoggerConfig, actionsDB firewalldb.ActionsWriteDB) (*RequestLogger, error) { hasInterceptorCaveat := func(caveats []string) bool { for _, c := range caveats { if strings.HasPrefix(c, macaroons.CondLndCustom) { return true } } return false } var shouldLogAction func(ri *RequestInfo) (bool, bool) switch cfg.RequestLoggerLevel { // Only log requests that have an interceptor caveat attached. case RequestLoggerLevelInterceptor: shouldLogAction = func(ri *RequestInfo) (bool, bool) { if hasInterceptorCaveat(ri.Caveats) { return true, true } return false, false } // Log all requests but only log request params if the request // has an interceptor caveat. case RequestLoggerLevelAll: shouldLogAction = func(ri *RequestInfo) (bool, bool) { return true, hasInterceptorCaveat(ri.Caveats) } // Log all requests will all request parameters. case RequestLoggerLevelFull: shouldLogAction = func(ri *RequestInfo) (bool, bool) { return true, true } default: return nil, fmt.Errorf("unknown request logger level: %s. "+ "Expected either 'interceptor', 'all' or 'full'", cfg.RequestLoggerLevel) } return &RequestLogger{ shouldLogAction: shouldLogAction, actionsDB: actionsDB, reqIDToAction: make(map[uint64]firewalldb.ActionLocator), }, nil } // Name returns the name of the interceptor. func (r *RequestLogger) Name() string { return RequestLoggerName } // ReadOnly returns true if this interceptor should be registered in read-only // mode. In read-only mode no custom caveat name can be specified. func (r *RequestLogger) ReadOnly() bool { return true } // CustomCaveatName returns the name of the custom caveat that is expected to be // handled by this interceptor. Cannot be specified in read-only mode. func (r *RequestLogger) CustomCaveatName() string { return "" } // Intercept processes an RPC middleware interception request and returns the // interception result which either accepts or rejects the intercepted message. func (r *RequestLogger) Intercept(ctx context.Context, req *lnrpc.RPCMiddlewareRequest) (*lnrpc.RPCMiddlewareResponse, error) { ri, err := NewInfoFromRequest(req) if err != nil { return nil, fmt.Errorf("error parsing incoming RPC middleware "+ "interception request: %v", err) } // If this request is for any URI in the uriSkipList map, then we do not // log or persist it. if uriSkipList[ri.URI] { return mid.RPCOk(req) } shouldLogAction, withPayloadData := r.shouldLogAction(ri) if !shouldLogAction { return mid.RPCOk(req) } log.Tracef("RequestLogger: Intercepting %v", ri) switch ri.MWRequestType { case MWRequestTypeStreamAuth: return mid.RPCOk(req) // Parse incoming requests and act on them. case MWRequestTypeRequest: return mid.RPCErr(req, r.addNewAction(ctx, ri, withPayloadData)) // Parse and possibly manipulate outgoing responses. case MWRequestTypeResponse: var ( state = firewalldb.ActionStateDone errReason string ) if ri.IsError { state = firewalldb.ActionStateError errReason = mid.ParseResponseErr(ri.Serialized).Error() } return mid.RPCErr( req, r.MarkAction(ctx, ri.RequestID, state, errReason), ) default: return mid.RPCErrString(req, "invalid intercept type: %v", r) } } // addNewAction persists the new action to the db. func (r *RequestLogger) addNewAction(ctx context.Context, ri *RequestInfo, withPayloadData bool) error { var ( rootKeyID fn.Option[uint64] ) if ri.Macaroon != nil { var err error fullRootKeyID, err := litmac.RootKeyIDFromMacaroon( ri.Macaroon, ) if err != nil { return fmt.Errorf("could not extract root key ID from "+ "macaroon: %w", err) } rootKeyID = fn.Some(fullRootKeyID) } actionReq := &firewalldb.AddActionReq{ SessionID: ri.SessionID, AccountID: ri.AccountID, MacaroonRootKeyID: rootKeyID, RPCMethod: ri.URI, } if withPayloadData { msg, err := mid.ParseProtobuf(ri.GRPCMessageType, ri.Serialized) if err != nil { return err } jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(msg) if err != nil { return fmt.Errorf("unable to decode response: %v", err) } actionReq.RPCParamsJson = jsonBytes meta := ri.MetaInfo if meta != nil { actionReq.ActorName = meta.ActorName actionReq.FeatureName = meta.Feature actionReq.Trigger = meta.Trigger actionReq.Intent = meta.Intent actionReq.StructuredJsonData = meta.StructuredJsonData } } locator, err := r.actionsDB.AddAction(ctx, actionReq) if err != nil { return err } r.mu.Lock() r.reqIDToAction[ri.RequestID] = locator r.mu.Unlock() return nil } // MarkAction can be used to set the state of an action identified by the given // requestID. func (r *RequestLogger) MarkAction(ctx context.Context, reqID uint64, state firewalldb.ActionState, errReason string) error { r.mu.Lock() defer r.mu.Unlock() actionLocator, ok := r.reqIDToAction[reqID] if !ok { return nil } delete(r.reqIDToAction, reqID) return r.actionsDB.SetActionState(ctx, actionLocator, state, errReason) }