Merge pull request #5101 from guggero/macaroon-interceptor

Add macaroon based RPC middleware interceptor
This commit is contained in:
Olaoluwa Osuntokun 2021-09-20 19:15:04 -07:00 committed by GitHub
commit 9264185f5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 3970 additions and 641 deletions

View file

@ -8,6 +8,7 @@ import (
"net"
"strconv"
"strings"
"unicode"
"github.com/golang/protobuf/proto"
"github.com/lightningnetwork/lnd/lncfg"
@ -65,6 +66,16 @@ var bakeMacaroonCommand = cli.Command{
Name: "ip_address",
Usage: "the IP address the macaroon will be bound to",
},
cli.StringFlag{
Name: "custom_caveat_name",
Usage: "the name of the custom caveat to add",
},
cli.StringFlag{
Name: "custom_caveat_condition",
Usage: "the condition of the custom caveat to add, " +
"can be empty if custom caveat doesn't need " +
"a value",
},
cli.Uint64Flag{
Name: "root_key_id",
Usage: "the numerical root key ID used to create the macaroon",
@ -92,6 +103,8 @@ func bakeMacaroon(ctx *cli.Context) error {
savePath string
timeout int64
ipAddress net.IP
customCaveatName string
customCaveatCond string
rootKeyID uint64
parsedPermissions []*lnrpc.MacaroonPermission
err error
@ -116,6 +129,32 @@ func bakeMacaroon(ctx *cli.Context) error {
}
}
if ctx.IsSet("custom_caveat_name") {
customCaveatName = ctx.String("custom_caveat_name")
if containsWhiteSpace(customCaveatName) {
return fmt.Errorf("unexpected white space found in " +
"custom caveat name")
}
if customCaveatName == "" {
return fmt.Errorf("invalid custom caveat name")
}
}
if ctx.IsSet("custom_caveat_condition") {
customCaveatCond = ctx.String("custom_caveat_condition")
if containsWhiteSpace(customCaveatCond) {
return fmt.Errorf("unexpected white space found in " +
"custom caveat condition")
}
if customCaveatCond == "" {
return fmt.Errorf("invalid custom caveat condition")
}
if customCaveatCond != "" && customCaveatName == "" {
return fmt.Errorf("cannot set custom caveat " +
"condition without custom caveat name")
}
}
if ctx.IsSet("root_key_id") {
rootKeyID = ctx.Uint64("root_key_id")
}
@ -186,6 +225,17 @@ func bakeMacaroon(ctx *cli.Context) error {
macaroons.IPLockConstraint(ipAddress.String()),
)
}
// The custom caveat condition is optional, it could just be a marker
// tag in the macaroon with just a name. The interceptor itself doesn't
// care about the value anyway.
if customCaveatName != "" {
macConstraints = append(
macConstraints, macaroons.CustomConstraint(
customCaveatName, customCaveatCond,
),
)
}
constrainedMac, err := macaroons.AddConstraints(
unmarshalMac, macConstraints...,
)
@ -419,3 +469,10 @@ func printMacaroon(ctx *cli.Context) error {
return nil
}
// containsWhiteSpace returns true if the given string contains any character
// that is considered to be a white space or non-printable character such as
// space, tabulator, newline, carriage return and some more exotic ones.
func containsWhiteSpace(str string) bool {
return strings.IndexFunc(str, unicode.IsSpace) >= 0
}

View file

@ -167,7 +167,10 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
}
// Now we append the macaroon credentials to the dial options.
cred := macaroons.NewMacaroonCredential(constrainedMac)
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
if err != nil {
fatal(fmt.Errorf("error cloning mac: %v", err))
}
opts = append(opts, grpc.WithPerRPCCredentials(cred))
}

View file

@ -380,6 +380,8 @@ type Config struct {
Cluster *lncfg.Cluster `group:"cluster" namespace:"cluster"`
RPCMiddleware *lncfg.RPCMiddleware `group:"rpcmiddleware" namespace:"rpcmiddleware"`
// LogWriter is the root logger that all of the daemon's subloggers are
// hooked up to.
LogWriter *build.RotatingLogWriter
@ -550,6 +552,7 @@ func DefaultConfig() Config {
LogWriter: build.NewRotatingLogWriter(),
DB: lncfg.DefaultDB(),
Cluster: lncfg.DefaultCluster(),
RPCMiddleware: lncfg.DefaultRPCMiddleware(),
registeredChains: chainreg.NewChainRegistry(),
ActiveNetParams: chainreg.BitcoinTestNetParams,
ChannelCommitInterval: defaultChannelCommitInterval,
@ -639,6 +642,7 @@ func LoadConfig(interceptor signal.Interceptor) (*Config, error) {
// normalized. The cleaned up config is returned on success.
func ValidateConfig(cfg Config, usageMessage string,
interceptor signal.Interceptor) (*Config, error) {
// If the provided lnd directory is not the default, we'll modify the
// path to all of the files and directories that will live within it.
lndDir := CleanAndExpandPath(cfg.LndDir)
@ -1477,6 +1481,7 @@ func ValidateConfig(cfg Config, usageMessage string,
cfg.DB,
cfg.Cluster,
cfg.HealthChecks,
cfg.RPCMiddleware,
)
if err != nil {
return nil, err

View file

@ -79,6 +79,12 @@ proposed channel type is used.
* [Adds an option to the BakeMacaroon rpc "allow-external-permissions,"](https://github.com/lightningnetwork/lnd/pull/5304) which makes it possible to bake a macaroon with external permissions. That way, the baked macaroons can be used for services beyond LND. Also adds a new CheckMacaroonPermissions rpc that checks that the macaroon permissions and other restrictions are being followed. It can also check permissions not native to LND.
* [A new RPC middleware
interceptor](https://github.com/lightningnetwork/lnd/pull/5101) was added that
allows external tools to hook into `lnd`'s RPC server and intercept any
requests made with custom macaroons (and also the responses to those
requests).
### Batched channel funding
[Multiple channels can now be opened in a single

40
lncfg/rpcmiddleware.go Normal file
View file

@ -0,0 +1,40 @@
package lncfg
import (
"fmt"
"time"
)
const (
// defaultRPCMiddlewareTimeout is the time after which a request sent to
// a gRPC interception middleware times out. This value is chosen very
// low since in a worst case scenario that time is added to a request's
// full duration twice (request and response interception) if a
// middleware is very slow.
defaultRPCMiddlewareTimeout = 2 * time.Second
)
// RPCMiddleware holds the configuration for RPC interception middleware.
type RPCMiddleware struct {
Enable bool `long:"enable" description:"Enable the RPC middleware interceptor functionality."`
InterceptTimeout time.Duration `long:"intercepttimeout" description:"Time after which a RPC middleware intercept request will time out and return an error if it hasn't yet received a response."`
Mandatory []string `long:"addmandatory" description:"Add the named middleware to the list of mandatory middlewares. All RPC requests are blocked/denied if any of the mandatory middlewares is not registered. Can be specified multiple times."`
}
// Validate checks the values configured for the RPC middleware.
func (r *RPCMiddleware) Validate() error {
if r.InterceptTimeout < 0 {
return fmt.Errorf("RPC middleware intercept timeout cannot " +
"be negative")
}
return nil
}
// DefaultRPCMiddleware returns the default values for the RPC interception
// middleware configuration.
func DefaultRPCMiddleware() *RPCMiddleware {
return &RPCMiddleware{
InterceptTimeout: defaultRPCMiddlewareTimeout,
}
}

8
lnd.go
View file

@ -111,7 +111,10 @@ func AdminAuthOptions(cfg *Config, skipMacaroons bool) ([]grpc.DialOption, error
}
// Now we append the macaroon credentials to the dial options.
cred := macaroons.NewMacaroonCredential(mac)
cred, err := macaroons.NewMacaroonCredential(mac)
if err != nil {
return nil, fmt.Errorf("error cloning mac: %v", err)
}
opts = append(opts, grpc.WithPerRPCCredentials(cred))
}
@ -349,7 +352,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error
// Create a new RPC interceptor that we'll add to the GRPC server. This
// will be used to log the API calls invoked on the GRPC server.
interceptorChain := rpcperms.NewInterceptorChain(
rpcsLog, cfg.NoMacaroons,
rpcsLog, cfg.NoMacaroons, cfg.RPCMiddleware.Mandatory,
)
if err := interceptorChain.Start(); err != nil {
return err
@ -579,6 +582,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, interceptor signal.Interceptor) error
macaroonService, err = macaroons.NewService(
dbs.macaroonDB, "lnd", walletInitParams.StatelessInit,
macaroons.IPLockChecker,
macaroons.CustomChecker(interceptorChain),
)
if err != nil {
err := fmt.Errorf("unable to set up macaroon "+

File diff suppressed because it is too large Load diff

View file

@ -2251,6 +2251,58 @@ func local_request_Lightning_CheckMacaroonPermissions_0(ctx context.Context, mar
}
func request_Lightning_RegisterRPCMiddleware_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (Lightning_RegisterRPCMiddlewareClient, runtime.ServerMetadata, error) {
var metadata runtime.ServerMetadata
stream, err := client.RegisterRPCMiddleware(ctx)
if err != nil {
grpclog.Infof("Failed to start streaming: %v", err)
return nil, metadata, err
}
dec := marshaler.NewDecoder(req.Body)
handleSend := func() error {
var protoReq RPCMiddlewareResponse
err := dec.Decode(&protoReq)
if err == io.EOF {
return err
}
if err != nil {
grpclog.Infof("Failed to decode request: %v", err)
return err
}
if err := stream.Send(&protoReq); err != nil {
grpclog.Infof("Failed to send request: %v", err)
return err
}
return nil
}
if err := handleSend(); err != nil {
if cerr := stream.CloseSend(); cerr != nil {
grpclog.Infof("Failed to terminate client stream: %v", cerr)
}
if err == io.EOF {
return stream, metadata, nil
}
return nil, metadata, err
}
go func() {
for {
if err := handleSend(); err != nil {
break
}
}
if err := stream.CloseSend(); err != nil {
grpclog.Infof("Failed to terminate client stream: %v", err)
}
}()
header, err := stream.Header()
if err != nil {
grpclog.Infof("Failed to get header from client: %v", err)
return nil, metadata, err
}
metadata.HeaderMD = header
return stream, metadata, nil
}
// RegisterLightningHandlerServer registers the http handlers for service Lightning to "mux".
// UnaryRPC :call LightningServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@ -3500,6 +3552,13 @@ func RegisterLightningHandlerServer(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_Lightning_RegisterRPCMiddleware_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport")
_, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
})
return nil
}
@ -4761,6 +4820,26 @@ func RegisterLightningHandlerClient(ctx context.Context, mux *runtime.ServeMux,
})
mux.Handle("POST", pattern_Lightning_RegisterRPCMiddleware_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req, "/lnrpc.Lightning/RegisterRPCMiddleware", runtime.WithHTTPPathPattern("/v1/middleware"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Lightning_RegisterRPCMiddleware_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Lightning_RegisterRPCMiddleware_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...)
})
return nil
}
@ -4886,6 +4965,8 @@ var (
pattern_Lightning_ListPermissions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "macaroon", "permissions"}, ""))
pattern_Lightning_CheckMacaroonPermissions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "macaroon", "checkpermissions"}, ""))
pattern_Lightning_RegisterRPCMiddleware_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "middleware"}, ""))
)
var (
@ -5010,4 +5091,6 @@ var (
forward_Lightning_ListPermissions_0 = runtime.ForwardResponseMessage
forward_Lightning_CheckMacaroonPermissions_0 = runtime.ForwardResponseMessage
forward_Lightning_RegisterRPCMiddleware_0 = runtime.ForwardResponseStream
)

View file

@ -540,6 +540,23 @@ service Lightning {
*/
rpc CheckMacaroonPermissions (CheckMacPermRequest)
returns (CheckMacPermResponse);
/*
RegisterRPCMiddleware adds a new gRPC middleware to the interceptor chain. A
gRPC middleware is software component external to lnd that aims to add
additional business logic to lnd by observing/intercepting/validating
incoming gRPC client requests and (if needed) replacing/overwriting outgoing
messages before they're sent to the client. When registering the middleware
must identify itself and indicate what custom macaroon caveats it wants to
be responsible for. Only requests that contain a macaroon with that specific
custom caveat are then sent to the middleware for inspection. The other
option is to register for the read-only mode in which all requests/responses
are forwarded for interception to the middleware but the middleware is not
allowed to modify any responses. As a security measure, _no_ middleware can
modify responses for requests made with _unencumbered_ macaroons!
*/
rpc RegisterRPCMiddleware (stream RPCMiddlewareResponse)
returns (stream RPCMiddlewareRequest);
}
message Utxo {
@ -4051,3 +4068,188 @@ message CheckMacPermRequest {
message CheckMacPermResponse {
bool valid = 1;
}
message RPCMiddlewareRequest {
/*
The unique ID of the intercepted request. Useful for mapping request to
response when implementing full duplex message interception.
*/
uint64 request_id = 1;
/*
The raw bytes of the complete macaroon as sent by the gRPC client in the
original request. This might be empty for a request that doesn't require
macaroons such as the wallet unlocker RPCs.
*/
bytes raw_macaroon = 2;
/*
The parsed condition of the macaroon's custom caveat for convenient access.
This field only contains the value of the custom caveat that the handling
middleware has registered itself for. The condition _must_ be validated for
messages of intercept_type stream_auth and request!
*/
string custom_caveat_condition = 3;
/*
There are three types of messages that will be sent to the middleware for
inspection and approval: Stream authentication, request and response
interception. The first two can only be accepted (=forward to main RPC
server) or denied (=return error to client). Intercepted responses can also
be replaced/overwritten.
*/
oneof intercept_type {
/*
Intercept stream authentication: each new streaming RPC call that is
initiated against lnd and contains the middleware's custom macaroon
caveat can be approved or denied based upon the macaroon in the stream
header. This message will only be sent for streaming RPCs, unary RPCs
must handle the macaroon authentication in the request interception to
avoid an additional message round trip between lnd and the middleware.
*/
StreamAuth stream_auth = 4;
/*
Intercept incoming gRPC client request message: all incoming messages,
both on streaming and unary RPCs, are forwarded to the middleware for
inspection. For unary RPC messages the middleware is also expected to
validate the custom macaroon caveat of the request.
*/
RPCMessage request = 5;
/*
Intercept outgoing gRPC response message: all outgoing messages, both on
streaming and unary RPCs, are forwarded to the middleware for inspection
and amendment. The response in this message is the original response as
it was generated by the main RPC server. It can either be accepted
(=forwarded to the client), replaced/overwritten with a new message of
the same type, or replaced by an error message.
*/
RPCMessage response = 6;
}
}
message StreamAuth {
/*
The full URI (in the format /<rpcpackage>.<ServiceName>/MethodName, for
example /lnrpc.Lightning/GetInfo) of the streaming RPC method that was just
established.
*/
string method_full_uri = 1;
}
message RPCMessage {
/*
The full URI (in the format /<rpcpackage>.<ServiceName>/MethodName, for
example /lnrpc.Lightning/GetInfo) of the RPC method the message was sent
to/from.
*/
string method_full_uri = 1;
/*
Indicates whether the message was sent over a streaming RPC method or not.
*/
bool stream_rpc = 2;
/*
The full canonical gRPC name of the message type (in the format
<rpcpackage>.TypeName, for example lnrpc.GetInfoRequest).
*/
string type_name = 3;
/*
The full content of the gRPC message, serialized in the binary protobuf
format.
*/
bytes serialized = 4;
}
message RPCMiddlewareResponse {
/*
The unique ID of the intercepted request that this response refers to. Must
always be set when giving feedback to an intercept but is ignored for the
initial registration message.
*/
uint64 request_id = 1;
/*
The middleware can only send two types of messages to lnd: The initial
registration message that identifies the middleware and after that only
feedback messages to requests sent to the middleware.
*/
oneof middleware_message {
/*
The registration message identifies the middleware that's being
registered in lnd. The registration message must be sent immediately
after initiating the RegisterRpcMiddleware stream, otherwise lnd will
time out the attempt and terminate the request. NOTE: The middleware
will only receive interception messages for requests that contain a
macaroon with the custom caveat that the middleware declares it is
responsible for handling in the registration message! As a security
measure, _no_ middleware can intercept requests made with _unencumbered_
macaroons!
*/
MiddlewareRegistration register = 2;
/*
The middleware received an interception request and gives feedback to
it. The request_id indicates what message the feedback refers to.
*/
InterceptFeedback feedback = 3;
}
}
message MiddlewareRegistration {
/*
The name of the middleware to register. The name should be as informative
as possible and is logged on registration.
*/
string middleware_name = 1;
/*
The name of the custom macaroon caveat that this middleware is responsible
for. Only requests/responses that contain a macaroon with the registered
custom caveat are forwarded for interception to the middleware. The
exception being the read-only mode: All requests/responses are forwarded to
a middleware that requests read-only access but such a middleware won't be
allowed to _alter_ responses. As a security measure, _no_ middleware can
change responses to requests made with _unencumbered_ macaroons!
NOTE: Cannot be used at the same time as read_only_mode.
*/
string custom_macaroon_caveat_name = 2;
/*
Instead of defining a custom macaroon caveat name a middleware can register
itself for read-only access only. In that mode all requests/responses are
forwarded to the middleware but the middleware isn't allowed to alter any of
the responses.
NOTE: Cannot be used at the same time as custom_macaroon_caveat_name.
*/
bool read_only_mode = 3;
}
message InterceptFeedback {
/*
The error to return to the user. If this is non-empty, the incoming gRPC
stream/request is aborted and the error is returned to the gRPC client. If
this value is empty, it means the middleware accepts the stream/request/
response and the processing of it can continue.
*/
string error = 1;
/*
A boolean indicating that the gRPC response should be replaced/overwritten.
As its name suggests, this can only be used as a feedback to an intercepted
response RPC message and is ignored for feedback on any other message. This
boolean is needed because in protobuf an empty message is serialized as a
0-length or nil byte slice and we wouldn't be able to distinguish between
an empty replacement message and the "don't replace anything" case.
*/
bool replace_response = 2;
/*
If the replace_response field is set to true, this field must contain the
binary serialized gRPC response message in the protobuf format.
*/
bytes replacement_serialized = 3;
}

View file

@ -1675,6 +1675,49 @@
]
}
},
"/v1/middleware": {
"post": {
"summary": "RegisterRPCMiddleware adds a new gRPC middleware to the interceptor chain. A\ngRPC middleware is software component external to lnd that aims to add\nadditional business logic to lnd by observing/intercepting/validating\nincoming gRPC client requests and (if needed) replacing/overwriting outgoing\nmessages before they're sent to the client. When registering the middleware\nmust identify itself and indicate what custom macaroon caveats it wants to\nbe responsible for. Only requests that contain a macaroon with that specific\ncustom caveat are then sent to the middleware for inspection. The other\noption is to register for the read-only mode in which all requests/responses\nare forwarded for interception to the middleware but the middleware is not\nallowed to modify any responses. As a security measure, _no_ middleware can\nmodify responses for requests made with _unencumbered_ macaroons!",
"operationId": "Lightning_RegisterRPCMiddleware",
"responses": {
"200": {
"description": "A successful response.(streaming responses)",
"schema": {
"type": "object",
"properties": {
"result": {
"$ref": "#/definitions/lnrpcRPCMiddlewareRequest"
},
"error": {
"$ref": "#/definitions/rpcStatus"
}
},
"title": "Stream result of lnrpcRPCMiddlewareRequest"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"description": " (streaming inputs)",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/lnrpcRPCMiddlewareResponse"
}
}
],
"tags": [
"Lightning"
]
}
},
"/v1/newaddress": {
"get": {
"summary": "lncli: `newaddress`\nNewAddress creates a new address under control of the local wallet.",
@ -4479,6 +4522,24 @@
],
"default": "INITIATOR_UNKNOWN"
},
"lnrpcInterceptFeedback": {
"type": "object",
"properties": {
"error": {
"type": "string",
"description": "The error to return to the user. If this is non-empty, the incoming gRPC\nstream/request is aborted and the error is returned to the gRPC client. If\nthis value is empty, it means the middleware accepts the stream/request/\nresponse and the processing of it can continue."
},
"replace_response": {
"type": "boolean",
"description": "A boolean indicating that the gRPC response should be replaced/overwritten.\nAs its name suggests, this can only be used as a feedback to an intercepted\nresponse RPC message and is ignored for feedback on any other message. This\nboolean is needed because in protobuf an empty message is serialized as a\n0-length or nil byte slice and we wouldn't be able to distinguish between\nan empty replacement message and the \"don't replace anything\" case."
},
"replacement_serialized": {
"type": "string",
"format": "byte",
"description": "If the replace_response field is set to true, this field must contain the\nbinary serialized gRPC response message in the protobuf format."
}
}
},
"lnrpcInvoice": {
"type": "object",
"properties": {
@ -4903,6 +4964,23 @@
}
}
},
"lnrpcMiddlewareRegistration": {
"type": "object",
"properties": {
"middleware_name": {
"type": "string",
"description": "The name of the middleware to register. The name should be as informative\nas possible and is logged on registration."
},
"custom_macaroon_caveat_name": {
"type": "string",
"description": "The name of the custom macaroon caveat that this middleware is responsible\nfor. Only requests/responses that contain a macaroon with the registered\ncustom caveat are forwarded for interception to the middleware. The\nexception being the read-only mode: All requests/responses are forwarded to\na middleware that requests read-only access but such a middleware won't be\nallowed to _alter_ responses. As a security measure, _no_ middleware can\nchange responses to requests made with _unencumbered_ macaroons!\nNOTE: Cannot be used at the same time as read_only_mode."
},
"read_only_mode": {
"type": "boolean",
"description": "Instead of defining a custom macaroon caveat name a middleware can register\nitself for read-only access only. In that mode all requests/responses are\nforwarded to the middleware but the middleware isn't allowed to alter any of\nthe responses.\nNOTE: Cannot be used at the same time as custom_macaroon_caveat_name."
}
}
},
"lnrpcMultiChanBackup": {
"type": "object",
"properties": {
@ -5626,6 +5704,77 @@
}
}
},
"lnrpcRPCMessage": {
"type": "object",
"properties": {
"method_full_uri": {
"type": "string",
"description": "The full URI (in the format /\u003crpcpackage\u003e.\u003cServiceName\u003e/MethodName, for\nexample /lnrpc.Lightning/GetInfo) of the RPC method the message was sent\nto/from."
},
"stream_rpc": {
"type": "boolean",
"description": "Indicates whether the message was sent over a streaming RPC method or not."
},
"type_name": {
"type": "string",
"description": "The full canonical gRPC name of the message type (in the format\n\u003crpcpackage\u003e.TypeName, for example lnrpc.GetInfoRequest)."
},
"serialized": {
"type": "string",
"format": "byte",
"description": "The full content of the gRPC message, serialized in the binary protobuf\nformat."
}
}
},
"lnrpcRPCMiddlewareRequest": {
"type": "object",
"properties": {
"request_id": {
"type": "string",
"format": "uint64",
"description": "The unique ID of the intercepted request. Useful for mapping request to\nresponse when implementing full duplex message interception."
},
"raw_macaroon": {
"type": "string",
"format": "byte",
"description": "The raw bytes of the complete macaroon as sent by the gRPC client in the\noriginal request. This might be empty for a request that doesn't require\nmacaroons such as the wallet unlocker RPCs."
},
"custom_caveat_condition": {
"type": "string",
"title": "The parsed condition of the macaroon's custom caveat for convenient access.\nThis field only contains the value of the custom caveat that the handling\nmiddleware has registered itself for. The condition _must_ be validated for\nmessages of intercept_type stream_auth and request!"
},
"stream_auth": {
"$ref": "#/definitions/lnrpcStreamAuth",
"description": "Intercept stream authentication: each new streaming RPC call that is\ninitiated against lnd and contains the middleware's custom macaroon\ncaveat can be approved or denied based upon the macaroon in the stream\nheader. This message will only be sent for streaming RPCs, unary RPCs\nmust handle the macaroon authentication in the request interception to\navoid an additional message round trip between lnd and the middleware."
},
"request": {
"$ref": "#/definitions/lnrpcRPCMessage",
"description": "Intercept incoming gRPC client request message: all incoming messages,\nboth on streaming and unary RPCs, are forwarded to the middleware for\ninspection. For unary RPC messages the middleware is also expected to\nvalidate the custom macaroon caveat of the request."
},
"response": {
"$ref": "#/definitions/lnrpcRPCMessage",
"description": "Intercept outgoing gRPC response message: all outgoing messages, both on\nstreaming and unary RPCs, are forwarded to the middleware for inspection\nand amendment. The response in this message is the original response as\nit was generated by the main RPC server. It can either be accepted\n(=forwarded to the client), replaced/overwritten with a new message of\nthe same type, or replaced by an error message."
}
}
},
"lnrpcRPCMiddlewareResponse": {
"type": "object",
"properties": {
"request_id": {
"type": "string",
"format": "uint64",
"description": "The unique ID of the intercepted request that this response refers to. Must\nalways be set when giving feedback to an intercept but is ignored for the\ninitial registration message."
},
"register": {
"$ref": "#/definitions/lnrpcMiddlewareRegistration",
"title": "The registration message identifies the middleware that's being\nregistered in lnd. The registration message must be sent immediately\nafter initiating the RegisterRpcMiddleware stream, otherwise lnd will\ntime out the attempt and terminate the request. NOTE: The middleware\nwill only receive interception messages for requests that contain a\nmacaroon with the custom caveat that the middleware declares it is\nresponsible for handling in the registration message! As a security\nmeasure, _no_ middleware can intercept requests made with _unencumbered_\nmacaroons!"
},
"feedback": {
"$ref": "#/definitions/lnrpcInterceptFeedback",
"description": "The middleware received an interception request and gives feedback to\nit. The request_id indicates what message the feedback refers to."
}
}
},
"lnrpcReadyForPsbtFunding": {
"type": "object",
"properties": {
@ -6047,6 +6196,15 @@
"lnrpcStopResponse": {
"type": "object"
},
"lnrpcStreamAuth": {
"type": "object",
"properties": {
"method_full_uri": {
"type": "string",
"description": "The full URI (in the format /\u003crpcpackage\u003e.\u003cServiceName\u003e/MethodName, for\nexample /lnrpc.Lightning/GetInfo) of the streaming RPC method that was just\nestablished."
}
}
},
"lnrpcTimestampedError": {
"type": "object",
"properties": {

View file

@ -153,4 +153,6 @@ http:
- selector: lnrpc.Lightning.CheckMacaroonPermissions
post: "/v1/macaroon/checkpermissions"
body: "*"
- selector: lnrpc.Lightning.RegisterRPCMiddleware
post: "/v1/middleware"
body: "*"

View file

@ -389,6 +389,20 @@ type LightningClient interface {
//imposed on the macaroon and that the macaroon is authorized to follow the
//provided permissions.
CheckMacaroonPermissions(ctx context.Context, in *CheckMacPermRequest, opts ...grpc.CallOption) (*CheckMacPermResponse, error)
//
//RegisterRPCMiddleware adds a new gRPC middleware to the interceptor chain. A
//gRPC middleware is software component external to lnd that aims to add
//additional business logic to lnd by observing/intercepting/validating
//incoming gRPC client requests and (if needed) replacing/overwriting outgoing
//messages before they're sent to the client. When registering the middleware
//must identify itself and indicate what custom macaroon caveats it wants to
//be responsible for. Only requests that contain a macaroon with that specific
//custom caveat are then sent to the middleware for inspection. The other
//option is to register for the read-only mode in which all requests/responses
//are forwarded for interception to the middleware but the middleware is not
//allowed to modify any responses. As a security measure, _no_ middleware can
//modify responses for requests made with _unencumbered_ macaroons!
RegisterRPCMiddleware(ctx context.Context, opts ...grpc.CallOption) (Lightning_RegisterRPCMiddlewareClient, error)
}
type lightningClient struct {
@ -1209,6 +1223,37 @@ func (c *lightningClient) CheckMacaroonPermissions(ctx context.Context, in *Chec
return out, nil
}
func (c *lightningClient) RegisterRPCMiddleware(ctx context.Context, opts ...grpc.CallOption) (Lightning_RegisterRPCMiddlewareClient, error) {
stream, err := c.cc.NewStream(ctx, &Lightning_ServiceDesc.Streams[11], "/lnrpc.Lightning/RegisterRPCMiddleware", opts...)
if err != nil {
return nil, err
}
x := &lightningRegisterRPCMiddlewareClient{stream}
return x, nil
}
type Lightning_RegisterRPCMiddlewareClient interface {
Send(*RPCMiddlewareResponse) error
Recv() (*RPCMiddlewareRequest, error)
grpc.ClientStream
}
type lightningRegisterRPCMiddlewareClient struct {
grpc.ClientStream
}
func (x *lightningRegisterRPCMiddlewareClient) Send(m *RPCMiddlewareResponse) error {
return x.ClientStream.SendMsg(m)
}
func (x *lightningRegisterRPCMiddlewareClient) Recv() (*RPCMiddlewareRequest, error) {
m := new(RPCMiddlewareRequest)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// LightningServer is the server API for Lightning service.
// All implementations must embed UnimplementedLightningServer
// for forward compatibility
@ -1584,6 +1629,20 @@ type LightningServer interface {
//imposed on the macaroon and that the macaroon is authorized to follow the
//provided permissions.
CheckMacaroonPermissions(context.Context, *CheckMacPermRequest) (*CheckMacPermResponse, error)
//
//RegisterRPCMiddleware adds a new gRPC middleware to the interceptor chain. A
//gRPC middleware is software component external to lnd that aims to add
//additional business logic to lnd by observing/intercepting/validating
//incoming gRPC client requests and (if needed) replacing/overwriting outgoing
//messages before they're sent to the client. When registering the middleware
//must identify itself and indicate what custom macaroon caveats it wants to
//be responsible for. Only requests that contain a macaroon with that specific
//custom caveat are then sent to the middleware for inspection. The other
//option is to register for the read-only mode in which all requests/responses
//are forwarded for interception to the middleware but the middleware is not
//allowed to modify any responses. As a security measure, _no_ middleware can
//modify responses for requests made with _unencumbered_ macaroons!
RegisterRPCMiddleware(Lightning_RegisterRPCMiddlewareServer) error
mustEmbedUnimplementedLightningServer()
}
@ -1777,6 +1836,9 @@ func (UnimplementedLightningServer) ListPermissions(context.Context, *ListPermis
func (UnimplementedLightningServer) CheckMacaroonPermissions(context.Context, *CheckMacPermRequest) (*CheckMacPermResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckMacaroonPermissions not implemented")
}
func (UnimplementedLightningServer) RegisterRPCMiddleware(Lightning_RegisterRPCMiddlewareServer) error {
return status.Errorf(codes.Unimplemented, "method RegisterRPCMiddleware not implemented")
}
func (UnimplementedLightningServer) mustEmbedUnimplementedLightningServer() {}
// UnsafeLightningServer may be embedded to opt out of forward compatibility for this service.
@ -2954,6 +3016,32 @@ func _Lightning_CheckMacaroonPermissions_Handler(srv interface{}, ctx context.Co
return interceptor(ctx, in, info, handler)
}
func _Lightning_RegisterRPCMiddleware_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(LightningServer).RegisterRPCMiddleware(&lightningRegisterRPCMiddlewareServer{stream})
}
type Lightning_RegisterRPCMiddlewareServer interface {
Send(*RPCMiddlewareRequest) error
Recv() (*RPCMiddlewareResponse, error)
grpc.ServerStream
}
type lightningRegisterRPCMiddlewareServer struct {
grpc.ServerStream
}
func (x *lightningRegisterRPCMiddlewareServer) Send(m *RPCMiddlewareRequest) error {
return x.ServerStream.SendMsg(m)
}
func (x *lightningRegisterRPCMiddlewareServer) Recv() (*RPCMiddlewareResponse, error) {
m := new(RPCMiddlewareResponse)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// Lightning_ServiceDesc is the grpc.ServiceDesc for Lightning service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -3225,6 +3313,12 @@ var Lightning_ServiceDesc = grpc.ServiceDesc{
Handler: _Lightning_SubscribeChannelBackups_Handler,
ServerStreams: true,
},
{
StreamName: "RegisterRPCMiddleware",
Handler: _Lightning_RegisterRPCMiddleware_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "lightning.proto",
}

View file

@ -9,6 +9,13 @@ import (
"github.com/lightningnetwork/lnd/lnwallet"
)
const (
// RegisterRPCMiddlewareURI is the full RPC method URI for the
// middleware registration call. This is declared here rather than where
// it's mainly used to avoid circular package dependencies.
RegisterRPCMiddlewareURI = "/lnrpc.Lightning/RegisterRPCMiddleware"
)
// RPCTransactionDetails returns a set of rpc transaction details.
func RPCTransactionDetails(txns []*lnwallet.TransactionDetail) *TransactionDetails {
txDetails := &TransactionDetails{

View file

@ -756,7 +756,7 @@ func (n *NetworkHarness) DisconnectNodes(a, b *HarnessNode) error {
func (n *NetworkHarness) RestartNode(node *HarnessNode, callback func() error,
chanBackups ...*lnrpc.ChanBackupSnapshot) error {
err := n.RestartNodeNoUnlock(node, callback)
err := n.RestartNodeNoUnlock(node, callback, true)
if err != nil {
return err
}
@ -794,7 +794,7 @@ func (n *NetworkHarness) RestartNode(node *HarnessNode, callback func() error,
// the callback parameter is non-nil, then the function will be executed after
// the node shuts down, but *before* the process has been started up again.
func (n *NetworkHarness) RestartNodeNoUnlock(node *HarnessNode,
callback func() error) error {
callback func() error, wait bool) error {
if err := node.stop(); err != nil {
return err
@ -806,7 +806,7 @@ func (n *NetworkHarness) RestartNodeNoUnlock(node *HarnessNode,
}
}
return node.start(n.lndBinary, n.lndErrorChan, true)
return node.start(n.lndBinary, n.lndErrorChan, wait)
}
// SuspendNode stops the given node and returns a callback that can be used to

View file

@ -657,7 +657,7 @@ func testStatelessInit(net *lntest.NetworkHarness, t *harnessTest) {
// As a second part, shut down the node and then try to change the
// password when we start it up again.
if err := net.RestartNodeNoUnlock(carol, nil); err != nil {
if err := net.RestartNodeNoUnlock(carol, nil, true); err != nil {
t.Fatalf("Node restart failed: %v", err)
}
changePwReq := &lnrpc.ChangePasswordRequest{

View file

@ -0,0 +1,621 @@
package itest
import (
"context"
"fmt"
"testing"
"time"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"gopkg.in/macaroon.v2"
)
// testRPCMiddlewareInterceptor tests that the RPC middleware interceptor can
// be used correctly and in a safe way.
func testRPCMiddlewareInterceptor(net *lntest.NetworkHarness, t *harnessTest) {
// Let's first enable the middleware interceptor.
net.Alice.Cfg.ExtraArgs = append(
net.Alice.Cfg.ExtraArgs, "--rpcmiddleware.enable",
)
err := net.RestartNode(net.Alice, nil)
require.NoError(t.t, err)
// Let's set up a channel between Alice and Bob, just to get some useful
// data to inspect when doing RPC calls to Alice later.
net.EnsureConnected(t.t, net.Alice, net.Bob)
net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, net.Alice)
_ = openChannelAndAssert(
t, net, net.Alice, net.Bob, lntest.OpenChannelParams{
Amt: 1_234_567,
},
)
// Load or bake the macaroons that the simulated users will use to
// access the RPC.
readonlyMac, err := net.Alice.ReadMacaroon(
net.Alice.ReadMacPath(), defaultTimeout,
)
require.NoError(t.t, err)
customCaveatMac, err := macaroons.SafeCopyMacaroon(readonlyMac)
require.NoError(t.t, err)
addConstraint := macaroons.CustomConstraint(
"itest-caveat", "itest-value",
)
require.NoError(t.t, addConstraint(customCaveatMac))
// Run all sub-tests now. We can't run anything in parallel because that
// would cause the main test function to exit and the nodes being
// cleaned up.
t.t.Run("registration restrictions", func(tt *testing.T) {
middlewareRegistrationRestrictionTests(tt, net.Alice)
})
t.t.Run("read-only intercept", func(tt *testing.T) {
registration := registerMiddleware(
tt, net.Alice, &lnrpc.MiddlewareRegistration{
MiddlewareName: "itest-interceptor",
ReadOnlyMode: true,
},
)
defer registration.cancel()
middlewareInterceptionTest(
tt, net.Alice, net.Bob, registration, readonlyMac,
customCaveatMac, true,
)
})
// We've manually disconnected Bob from Alice in the previous test, make
// sure they're connected again.
net.EnsureConnected(t.t, net.Alice, net.Bob)
t.t.Run("encumbered macaroon intercept", func(tt *testing.T) {
registration := registerMiddleware(
tt, net.Alice, &lnrpc.MiddlewareRegistration{
MiddlewareName: "itest-interceptor",
CustomMacaroonCaveatName: "itest-caveat",
},
)
defer registration.cancel()
middlewareInterceptionTest(
tt, net.Alice, net.Bob, registration, customCaveatMac,
readonlyMac, false,
)
})
// Next, run the response manipulation tests.
net.EnsureConnected(t.t, net.Alice, net.Bob)
t.t.Run("read-only not allowed to manipulate", func(tt *testing.T) {
registration := registerMiddleware(
tt, net.Alice, &lnrpc.MiddlewareRegistration{
MiddlewareName: "itest-interceptor",
ReadOnlyMode: true,
},
)
defer registration.cancel()
middlewareManipulationTest(
tt, net.Alice, net.Bob, registration, readonlyMac, true,
)
})
net.EnsureConnected(t.t, net.Alice, net.Bob)
t.t.Run("encumbered macaroon manipulate", func(tt *testing.T) {
registration := registerMiddleware(
tt, net.Alice, &lnrpc.MiddlewareRegistration{
MiddlewareName: "itest-interceptor",
CustomMacaroonCaveatName: "itest-caveat",
},
)
defer registration.cancel()
middlewareManipulationTest(
tt, net.Alice, net.Bob, registration, customCaveatMac,
false,
)
})
// And finally make sure mandatory middleware is always checked for any
// RPC request.
t.t.Run("mandatory middleware", func(tt *testing.T) {
middlewareMandatoryTest(tt, net.Alice, net)
})
}
// middlewareRegistrationRestrictionTests tests all restrictions that apply to
// registering a middleware.
func middlewareRegistrationRestrictionTests(t *testing.T,
node *lntest.HarnessNode) {
testCases := []struct {
registration *lnrpc.MiddlewareRegistration
expectedErr string
}{{
registration: &lnrpc.MiddlewareRegistration{
MiddlewareName: "foo",
},
expectedErr: "invalid middleware name",
}, {
registration: &lnrpc.MiddlewareRegistration{
MiddlewareName: "itest-interceptor",
CustomMacaroonCaveatName: "foo",
},
expectedErr: "custom caveat name of at least",
}, {
registration: &lnrpc.MiddlewareRegistration{
MiddlewareName: "itest-interceptor",
CustomMacaroonCaveatName: "itest-caveat",
ReadOnlyMode: true,
},
expectedErr: "cannot set read-only and custom caveat name",
}}
for idx, tc := range testCases {
tc := tc
t.Run(fmt.Sprintf("%d", idx), func(tt *testing.T) {
invalidName := registerMiddleware(
tt, node, tc.registration,
)
_, err := invalidName.stream.Recv()
require.Error(tt, err)
require.Contains(tt, err.Error(), tc.expectedErr)
invalidName.cancel()
})
}
}
// middlewareInterceptionTest tests that unary and streaming requests can be
// intercepted. It also makes sure that depending on the mode (read-only or
// custom macaroon caveat) a middleware only gets access to the requests it
// should be allowed access to.
func middlewareInterceptionTest(t *testing.T, node *lntest.HarnessNode,
peer *lntest.HarnessNode, registration *middlewareHarness,
userMac *macaroon.Macaroon, disallowedMac *macaroon.Macaroon,
readOnly bool) {
// Everything we test here should be executed in a matter of
// milliseconds, so we can use one single timeout context for all calls.
ctxb := context.Background()
ctxc, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
// Create a client connection that we'll use to simulate user requests
// to lnd with.
cleanup, client := macaroonClient(t, node, userMac)
defer cleanup()
// We're going to send a simple RPC request to list all channels.
// We need to invoke the intercept logic in a goroutine because we'd
// block the execution of the main task otherwise.
req := &lnrpc.ListChannelsRequest{ActiveOnly: true}
go registration.interceptUnary(
"/lnrpc.Lightning/ListChannels", req, nil,
)
// Do the actual call now and wait for the interceptor to do its thing.
resp, err := client.ListChannels(ctxc, req)
require.NoError(t, err)
// Did we receive the correct intercept message?
assertInterceptedType(t, resp, <-registration.responsesChan)
// Let's test the same for a streaming endpoint.
req2 := &lnrpc.PeerEventSubscription{}
go registration.interceptStream(
"/lnrpc.Lightning/SubscribePeerEvents", req2, nil,
)
// Do the actual call now and wait for the interceptor to do its thing.
peerCtx, peerCancel := context.WithCancel(ctxb)
resp2, err := client.SubscribePeerEvents(peerCtx, req2)
require.NoError(t, err)
// Disconnect Bob to trigger a peer event without using Alice's RPC
// interface itself.
_, err = peer.DisconnectPeer(ctxc, &lnrpc.DisconnectPeerRequest{
PubKey: node.PubKeyStr,
})
require.NoError(t, err)
peerEvent, err := resp2.Recv()
require.NoError(t, err)
require.Equal(t, lnrpc.PeerEvent_PEER_OFFLINE, peerEvent.GetType())
// Stop the peer stream again, otherwise we'll produce more events.
peerCancel()
// Did we receive the correct intercept message?
assertInterceptedType(t, peerEvent, <-registration.responsesChan)
// Make sure that with the other macaroon we aren't allowed to access
// the interceptor. If we registered for read-only access then there is
// no middleware that handles the custom macaroon caveat. If we
// registered for a custom caveat then there is no middleware that
// handles unencumbered read-only access.
cleanup, client = macaroonClient(t, node, disallowedMac)
defer cleanup()
// We need to make sure we don't get any interception messages for
// requests made with the disallowed macaroon.
var (
errChan = make(chan error, 1)
msgChan = make(chan *lnrpc.RPCMiddlewareRequest, 1)
)
go func() {
req, err := registration.stream.Recv()
if err != nil {
errChan <- err
return
}
msgChan <- req
}()
// Let's invoke the same request again but with the other macaroon.
resp, err = client.ListChannels(ctxc, req)
// Depending on what mode we're in, we expect something different. If we
// are in read-only mode then an encumbered macaroon cannot be used
// since there is no middleware registered for it. If we registered for
// a custom macaroon caveat and a request with anon-encumbered macaroon
// comes in, we expect to just not get any intercept messages.
if readOnly {
require.Error(t, err)
require.Contains(
t, err.Error(), "cannot accept macaroon with custom "+
"caveat 'itest-caveat', no middleware "+
"registered",
)
} else {
require.NoError(t, err)
// We disconnected Bob so there should be no active channels.
require.Len(t, resp.Channels, 0)
}
// There should be neither an error nor any interception messages in the
// channels.
select {
case err := <-errChan:
t.Fatalf("Unexpected error, not expecting messages: %v", err)
case msg := <-msgChan:
t.Fatalf("Unexpected intercept message: %v", msg)
case <-time.After(time.Second):
// Nothing came in for a second, we're fine.
}
}
// middlewareManipulationTest tests that unary and streaming requests can be
// intercepted and also manipulated, at least if the middleware didn't register
// for read-only access.
func middlewareManipulationTest(t *testing.T, node *lntest.HarnessNode,
peer *lntest.HarnessNode, registration *middlewareHarness,
userMac *macaroon.Macaroon, readOnly bool) {
// Everything we test here should be executed in a matter of
// milliseconds, so we can use one single timeout context for all calls.
ctxb := context.Background()
ctxc, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
// Create a client connection that we'll use to simulate user requests
// to lnd with.
cleanup, client := macaroonClient(t, node, userMac)
defer cleanup()
// We're going to attempt to replace the response with our own. But
// since we only registered for read-only access, our replacement should
// just be ignored.
replacementResponse := &lnrpc.ListChannelsResponse{
Channels: []*lnrpc.Channel{{
ChannelPoint: "f000:0",
}, {
ChannelPoint: "f000:1",
}},
}
// We're going to send a simple RPC request to list all channels.
// We need to invoke the intercept logic in a goroutine because we'd
// block the execution of the main task otherwise.
req := &lnrpc.ListChannelsRequest{ActiveOnly: true}
go registration.interceptUnary(
"/lnrpc.Lightning/ListChannels", req, replacementResponse,
)
// Do the actual call now and wait for the interceptor to do its thing.
resp, err := client.ListChannels(ctxc, req)
require.NoError(t, err)
// Did we get the manipulated response (2 fake channels) or the original
// one (1 channel)?
if readOnly {
require.Len(t, resp.Channels, 1)
} else {
require.Len(t, resp.Channels, 2)
}
// Let's test the same for a streaming endpoint.
replacementResponse2 := &lnrpc.PeerEvent{
Type: lnrpc.PeerEvent_PEER_ONLINE,
PubKey: "foo",
}
req2 := &lnrpc.PeerEventSubscription{}
go registration.interceptStream(
"/lnrpc.Lightning/SubscribePeerEvents", req2,
replacementResponse2,
)
// Do the actual call now and wait for the interceptor to do its thing.
peerCtx, peerCancel := context.WithCancel(ctxb)
resp2, err := client.SubscribePeerEvents(peerCtx, req2)
require.NoError(t, err)
// Disconnect Bob to trigger a peer event without using Alice's RPC
// interface itself.
_, err = peer.DisconnectPeer(ctxc, &lnrpc.DisconnectPeerRequest{
PubKey: node.PubKeyStr,
})
require.NoError(t, err)
peerEvent, err := resp2.Recv()
require.NoError(t, err)
// Did we get the correct, original response?
if readOnly {
require.Equal(
t, lnrpc.PeerEvent_PEER_OFFLINE, peerEvent.GetType(),
)
require.Equal(t, peer.PubKeyStr, peerEvent.PubKey)
} else {
require.Equal(
t, lnrpc.PeerEvent_PEER_ONLINE, peerEvent.GetType(),
)
require.Equal(t, "foo", peerEvent.PubKey)
}
// Stop the peer stream again, otherwise we'll produce more events.
peerCancel()
}
// middlewareMandatoryTest tests that all RPC requests are blocked if there is
// a mandatory middleware declared that's currently not registered.
func middlewareMandatoryTest(t *testing.T, node *lntest.HarnessNode,
net *lntest.NetworkHarness) {
// Let's declare our itest interceptor as mandatory but don't register
// it just yet. That should cause all RPC requests to fail, except for
// the registration itself.
node.Cfg.ExtraArgs = append(
node.Cfg.ExtraArgs,
"--rpcmiddleware.addmandatory=itest-interceptor",
)
err := net.RestartNodeNoUnlock(node, nil, false)
require.NoError(t, err)
// The "wait for node to start" flag of the above restart does too much
// and has a call to GetInfo built in, which will fail in this special
// test case. So we need to do the wait and client setup manually here.
conn, err := node.ConnectRPC(true)
require.NoError(t, err)
err = node.WaitUntilStarted(conn, defaultTimeout)
require.NoError(t, err)
node.LightningClient = lnrpc.NewLightningClient(conn)
ctxb := context.Background()
ctxc, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
// Test a unary request first.
_, err = node.ListChannels(ctxc, &lnrpc.ListChannelsRequest{})
require.Error(t, err)
require.Contains(
t, err.Error(), "middleware 'itest-interceptor' is "+
"currently not registered",
)
// Then a streaming one.
stream, err := node.SubscribeInvoices(ctxc, &lnrpc.InvoiceSubscription{})
require.NoError(t, err)
_, err = stream.Recv()
require.Error(t, err)
require.Contains(
t, err.Error(), "middleware 'itest-interceptor' is "+
"currently not registered",
)
// Now let's register the middleware and try again.
registration := registerMiddleware(
t, node, &lnrpc.MiddlewareRegistration{
MiddlewareName: "itest-interceptor",
CustomMacaroonCaveatName: "itest-caveat",
},
)
defer registration.cancel()
// Both the unary and streaming requests should now be allowed.
time.Sleep(500 * time.Millisecond)
_, err = node.ListChannels(ctxc, &lnrpc.ListChannelsRequest{})
require.NoError(t, err)
_, err = node.SubscribeInvoices(ctxc, &lnrpc.InvoiceSubscription{})
require.NoError(t, err)
// We now shut down the node manually to prevent the test from failing
// because we can't call the stop RPC if we unregister the middleware in
// the defer statement above.
err = net.ShutdownNode(node)
require.NoError(t, err)
}
// assertInterceptedType makes sure that the intercept message sent by the RPC
// interceptor is correct for a proto message that was sent or received over the
// RPC interface.
func assertInterceptedType(t *testing.T, rpcMessage proto.Message,
interceptMessage *lnrpc.RPCMessage) {
t.Helper()
require.Equal(
t, string(proto.MessageName(rpcMessage)),
interceptMessage.TypeName,
)
rawRequest, err := proto.Marshal(rpcMessage)
require.NoError(t, err)
// Make sure we don't trip over nil vs. empty slice in the equality
// check below.
if len(rawRequest) == 0 {
rawRequest = nil
}
require.Equal(t, rawRequest, interceptMessage.Serialized)
}
// middlewareStream is a type alias to shorten the long definition.
type middlewareStream lnrpc.Lightning_RegisterRPCMiddlewareClient
// middlewareHarness is a test harness that holds one instance of a simulated
// middleware.
type middlewareHarness struct {
t *testing.T
cancel func()
stream middlewareStream
responsesChan chan *lnrpc.RPCMessage
}
// registerMiddleware creates a new middleware harness and sends the initial
// register message to the RPC server.
func registerMiddleware(t *testing.T, node *lntest.HarnessNode,
registration *lnrpc.MiddlewareRegistration) *middlewareHarness {
ctxc, cancel := context.WithCancel(context.Background())
middlewareStream, err := node.RegisterRPCMiddleware(ctxc)
require.NoError(t, err)
err = middlewareStream.Send(&lnrpc.RPCMiddlewareResponse{
MiddlewareMessage: &lnrpc.RPCMiddlewareResponse_Register{
Register: registration,
},
})
require.NoError(t, err)
return &middlewareHarness{
t: t,
cancel: cancel,
stream: middlewareStream,
responsesChan: make(chan *lnrpc.RPCMessage),
}
}
// interceptUnary intercepts a unary call, optionally requesting to replace the
// response sent to the client. A unary call is expected to receive one
// intercept message for the request and one for the response.
//
// NOTE: Must be called in a goroutine as this will block until the response is
// read from the response channel.
func (h *middlewareHarness) interceptUnary(methodURI string,
expectedRequest proto.Message, responseReplacement proto.Message) {
// Read intercept message and make sure it's for an RPC request.
reqIntercept, err := h.stream.Recv()
require.NoError(h.t, err)
req := reqIntercept.GetRequest()
require.NotNil(h.t, req)
// We know the request we're going to send so make sure we get the right
// type and content from the interceptor.
require.Equal(h.t, methodURI, req.MethodFullUri)
assertInterceptedType(h.t, expectedRequest, req)
// We need to accept the request.
h.sendAccept(reqIntercept.RequestId, nil)
// Now read the intercept message for the response.
respIntercept, err := h.stream.Recv()
require.NoError(h.t, err)
res := respIntercept.GetResponse()
require.NotNil(h.t, res)
// We need to accept the response as well.
h.sendAccept(respIntercept.RequestId, responseReplacement)
h.responsesChan <- res
}
// interceptStream intercepts a streaming call, optionally requesting to replace
// the (first) response sent to the client. A streaming call is expected to
// receive one intercept message for the stream authentication, one for the
// first request and one for the first response.
//
// NOTE: Must be called in a goroutine as this will block until the first
// response is read from the response channel.
func (h *middlewareHarness) interceptStream(methodURI string,
expectedRequest proto.Message, responseReplacement proto.Message) {
// Read intercept message and make sure it's for an RPC stream auth.
authIntercept, err := h.stream.Recv()
require.NoError(h.t, err)
auth := authIntercept.GetStreamAuth()
require.NotNil(h.t, auth)
// This is just the authentication, so we can only look at the URI.
require.Equal(h.t, methodURI, auth.MethodFullUri)
// We need to accept the auth.
h.sendAccept(authIntercept.RequestId, nil)
// Read intercept message and make sure it's for an RPC request.
reqIntercept, err := h.stream.Recv()
require.NoError(h.t, err)
req := reqIntercept.GetRequest()
require.NotNil(h.t, req)
// We know the request we're going to send so make sure we get the right
// type and content from the interceptor.
require.Equal(h.t, methodURI, req.MethodFullUri)
assertInterceptedType(h.t, expectedRequest, req)
// We need to accept the request.
h.sendAccept(reqIntercept.RequestId, nil)
// Now read the intercept message for the response.
respIntercept, err := h.stream.Recv()
require.NoError(h.t, err)
res := respIntercept.GetResponse()
require.NotNil(h.t, res)
// We need to accept the response as well.
h.sendAccept(respIntercept.RequestId, responseReplacement)
h.responsesChan <- res
}
// sendAccept sends an accept feedback to the RPC server.
func (h *middlewareHarness) sendAccept(requestID uint64,
responseReplacement proto.Message) {
var replacementBytes []byte
if responseReplacement != nil {
var err error
replacementBytes, err = proto.Marshal(responseReplacement)
require.NoError(h.t, err)
}
err := h.stream.Send(&lnrpc.RPCMiddlewareResponse{
MiddlewareMessage: &lnrpc.RPCMiddlewareResponse_Feedback{
Feedback: &lnrpc.InterceptFeedback{
ReplaceResponse: len(replacementBytes) > 0,
ReplacementSerialized: replacementBytes,
},
},
RequestId: requestID,
})
require.NoError(h.t, err)
}

View file

@ -338,4 +338,8 @@ var allTestCases = []*testCase{
name: "max htlc pathfind",
test: testMaxHtlcPathfind,
},
{
name: "rpc middleware interceptor",
test: testRPCMiddlewareInterceptor,
},
}

View file

@ -802,7 +802,7 @@ func (hn *HarnessNode) start(lndBinary string, lndError chan<- error,
return err
}
if err := hn.waitUntilStarted(conn, DefaultTimeout); err != nil {
if err := hn.WaitUntilStarted(conn, DefaultTimeout); err != nil {
return err
}
@ -818,8 +818,8 @@ func (hn *HarnessNode) start(lndBinary string, lndError chan<- error,
return hn.initLightningClient(conn)
}
// waitUntilStarted waits until the wallet state flips from "WAITING_TO_START".
func (hn *HarnessNode) waitUntilStarted(conn grpc.ClientConnInterface,
// WaitUntilStarted waits until the wallet state flips from "WAITING_TO_START".
func (hn *HarnessNode) WaitUntilStarted(conn grpc.ClientConnInterface,
timeout time.Duration) error {
stateClient := lnrpc.NewStateClient(conn)
@ -840,6 +840,7 @@ func (hn *HarnessNode) waitUntilStarted(conn grpc.ClientConnInterface,
resp, err := stateStream.Recv()
if err != nil {
errChan <- err
return
}
if resp.State != lnrpc.WalletState_WAITING_TO_START {
@ -880,7 +881,7 @@ func (hn *HarnessNode) WaitUntilLeader(timeout time.Duration) error {
}
timeout -= time.Since(startTs)
if err := hn.waitUntilStarted(conn, timeout); err != nil {
if err := hn.WaitUntilStarted(conn, timeout); err != nil {
return err
}
@ -1195,7 +1196,10 @@ func (hn *HarnessNode) ConnectRPCWithMacaroon(mac *macaroon.Macaroon) (
if mac == nil {
return grpc.DialContext(ctx, hn.Cfg.RPCAddr(), opts...)
}
macCred := macaroons.NewMacaroonCredential(mac)
macCred, err := macaroons.NewMacaroonCredential(mac)
if err != nil {
return nil, fmt.Errorf("error cloning mac: %v", err)
}
opts = append(opts, grpc.WithPerRPCCredentials(macCred))
return grpc.DialContext(ctx, hn.Cfg.RPCAddr(), opts...)
@ -1291,7 +1295,9 @@ func (hn *HarnessNode) stop() error {
// Close any attempts at further grpc connections.
if hn.conn != nil {
err := hn.conn.Close()
if err != nil {
if err != nil &&
!strings.Contains(err.Error(), "connection is closing") {
return fmt.Errorf("error attempting to stop grpc "+
"client: %v", err)
}

2
log.go
View file

@ -37,6 +37,7 @@ import (
"github.com/lightningnetwork/lnd/peernotifier"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/localchans"
"github.com/lightningnetwork/lnd/rpcperms"
"github.com/lightningnetwork/lnd/signal"
"github.com/lightningnetwork/lnd/sweep"
"github.com/lightningnetwork/lnd/watchtower"
@ -159,6 +160,7 @@ func SetupLoggers(root *build.RotatingLogWriter, interceptor signal.Interceptor)
AddSubLogger(root, chanacceptor.Subsystem, interceptor, chanacceptor.UseLogger)
AddSubLogger(root, funding.Subsystem, interceptor, funding.UseLogger)
AddSubLogger(root, cluster.Subsystem, interceptor, cluster.UseLogger)
AddSubLogger(root, rpcperms.Subsystem, interceptor, rpcperms.UseLogger)
}
// AddSubLogger is a helper method to conveniently create and register the

View file

@ -37,8 +37,17 @@ func (m MacaroonCredential) GetRequestMetadata(ctx context.Context,
// NewMacaroonCredential returns a copy of the passed macaroon wrapped in a
// MacaroonCredential struct which implements PerRPCCredentials.
func NewMacaroonCredential(m *macaroon.Macaroon) MacaroonCredential {
func NewMacaroonCredential(m *macaroon.Macaroon) (MacaroonCredential, error) {
ms := MacaroonCredential{}
ms.Macaroon = m.Clone()
return ms
// The macaroon library's Clone() method has a subtle bug that doesn't
// correctly clone all caveats. We need to use our own, safe clone
// function instead.
var err error
ms.Macaroon, err = SafeCopyMacaroon(m)
if err != nil {
return ms, err
}
return ms, nil
}

View file

@ -1,9 +1,11 @@
package macaroons
import (
"bytes"
"context"
"fmt"
"net"
"strings"
"time"
"google.golang.org/grpc/peer"
@ -12,6 +14,29 @@ import (
macaroon "gopkg.in/macaroon.v2"
)
const (
// CondLndCustom is the first party caveat condition name that is used
// for all custom caveats in lnd. Every custom caveat entry will be
// encoded as the string
// "lnd-custom <custom-caveat-name> <custom-caveat-condition>"
// in the serialized macaroon. We choose a single space as the delimiter
// between the because that is also used by the macaroon bakery library.
CondLndCustom = "lnd-custom"
)
// CustomCaveatAcceptor is an interface that contains a single method for
// checking whether a macaroon with the given custom caveat name should be
// accepted or not.
type CustomCaveatAcceptor interface {
// CustomCaveatSupported returns nil if a macaroon with the given custom
// caveat name can be validated by any component in lnd (for example an
// RPC middleware). If no component is registered to handle the given
// custom caveat then an error must be returned. This method only checks
// the availability of a validating component, not the validity of the
// macaroon itself.
CustomCaveatSupported(customCaveatName string) error
}
// Constraint type adds a layer of indirection over macaroon caveats.
type Constraint func(*macaroon.Macaroon) error
@ -22,8 +47,17 @@ type Checker func() (string, checkers.Func)
// AddConstraints returns new derived macaroon by applying every passed
// constraint and tightening its restrictions.
func AddConstraints(mac *macaroon.Macaroon, cs ...Constraint) (*macaroon.Macaroon, error) {
newMac := mac.Clone()
func AddConstraints(mac *macaroon.Macaroon,
cs ...Constraint) (*macaroon.Macaroon, error) {
// The macaroon library's Clone() method has a subtle bug that doesn't
// correctly clone all caveats. We need to use our own, safe clone
// function instead.
newMac, err := SafeCopyMacaroon(mac)
if err != nil {
return nil, err
}
for _, constraint := range cs {
if err := constraint(newMac); err != nil {
return nil, err
@ -55,7 +89,8 @@ func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error {
if ipAddr != "" {
macaroonIPAddr := net.ParseIP(ipAddr)
if macaroonIPAddr == nil {
return fmt.Errorf("incorrect macaroon IP-lock address")
return fmt.Errorf("incorrect macaroon IP-" +
"lock address")
}
caveat := checkers.Condition("ipaddr",
macaroonIPAddr.String())
@ -87,3 +122,97 @@ func IPLockChecker() (string, checkers.Func) {
return nil
}
}
// CustomConstraint returns a function that adds a custom caveat condition to
// a macaroon.
func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error {
return func(mac *macaroon.Macaroon) error {
// We rely on a name being set for the interception, so don't
// allow creating a caveat without a name in the first place.
if name == "" {
return fmt.Errorf("name cannot be empty")
}
// The inner (custom) condition is optional.
outerCondition := fmt.Sprintf("%s %s", name, condition)
if condition == "" {
outerCondition = name
}
caveat := checkers.Condition(CondLndCustom, outerCondition)
return mac.AddFirstPartyCaveat([]byte(caveat))
}
}
// CustomChecker returns a Checker function that is used by the macaroon bakery
// library to check whether a custom caveat is supported by lnd in general or
// not. Support in this context means: An additional gRPC interceptor was set up
// that validates the content (=condition) of the custom caveat. If such an
// interceptor is in place then the acceptor should return a nil error. If no
// interceptor exists for the custom caveat in the macaroon of a request context
// then a non-nil error should be returned and the macaroon is rejected as a
// whole.
func CustomChecker(acceptor CustomCaveatAcceptor) Checker {
// We return the general name of all lnd custom macaroons and a function
// that splits the outer condition to extract the name of the custom
// condition and the condition itself. In the bakery library that's used
// here, a caveat always has the following form:
//
// <condition-name> <condition-value>
//
// Because a checker function needs to be bound to the condition name we
// have to choose a static name for the first part ("lnd-custom", see
// CondLndCustom. Otherwise we'd need to register a new Checker function
// for each custom caveat that's registered. To allow for a generic
// custom caveat handling, we just add another layer and expand the
// initial <condition-value> into
//
// "<custom-condition-name> <custom-condition-value>"
//
// The full caveat string entry of a macaroon that uses this generic
// mechanism would therefore look like this:
//
// "lnd-custom <custom-condition-name> <custom-condition-value>"
checker := func(_ context.Context, _, outerCondition string) error {
if outerCondition != strings.TrimSpace(outerCondition) {
return fmt.Errorf("unexpected white space found in " +
"caveat condition")
}
if outerCondition == "" {
return fmt.Errorf("expected custom caveat, got empty " +
"string")
}
// The condition part of the original caveat is now name and
// condition of the custom caveat (we add a layer of conditions
// to allow one custom checker to work for all custom lnd
// conditions that implement arbitrary business logic).
parts := strings.Split(outerCondition, " ")
customCaveatName := parts[0]
return acceptor.CustomCaveatSupported(customCaveatName)
}
return func() (string, checkers.Func) {
return CondLndCustom, checker
}
}
// HasCustomCaveat tests if the given macaroon has a custom caveat with the
// given custom caveat name.
func HasCustomCaveat(mac *macaroon.Macaroon, customCaveatName string) bool {
if mac == nil {
return false
}
caveatPrefix := []byte(fmt.Sprintf(
"%s %s", CondLndCustom, customCaveatName,
))
for _, caveat := range mac.Caveats() {
if bytes.HasPrefix(caveat.Id, caveatPrefix) {
return true
}
}
return false
}

View file

@ -6,6 +6,8 @@ import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/lightningnetwork/lnd/macaroons"
macaroon "gopkg.in/macaroon.v2"
)
@ -112,3 +114,34 @@ func TestIPLockBadIP(t *testing.T) {
t.Fatalf("IPLockConstraint with bad IP should fail.")
}
}
// TestCustomConstraint tests that a custom constraint with a name and value can
// be added to a macaroon.
func TestCustomConstraint(t *testing.T) {
// Test a custom caveat with a value first.
constraintFunc := macaroons.CustomConstraint("unit-test", "test-value")
testMacaroon := createDummyMacaroon(t)
require.NoError(t, constraintFunc(testMacaroon))
require.Equal(
t, []byte("lnd-custom unit-test test-value"),
testMacaroon.Caveats()[0].Id,
)
require.True(t, macaroons.HasCustomCaveat(testMacaroon, "unit-test"))
require.False(t, macaroons.HasCustomCaveat(testMacaroon, "test-value"))
require.False(t, macaroons.HasCustomCaveat(testMacaroon, "something"))
require.False(t, macaroons.HasCustomCaveat(nil, "foo"))
// Custom caveats don't necessarily need a value, just the name is fine
// too to create a tagged macaroon.
constraintFunc = macaroons.CustomConstraint("unit-test", "")
testMacaroon = createDummyMacaroon(t)
require.NoError(t, constraintFunc(testMacaroon))
require.Equal(
t, []byte("lnd-custom unit-test"), testMacaroon.Caveats()[0].Id,
)
require.True(t, macaroons.HasCustomCaveat(testMacaroon, "unit-test"))
require.False(t, macaroons.HasCustomCaveat(testMacaroon, "test-value"))
require.False(t, macaroons.HasCustomCaveat(testMacaroon, "something"))
}

View file

@ -4,10 +4,8 @@ import (
"context"
"encoding/hex"
"fmt"
"github.com/lightningnetwork/lnd/kvdb"
"google.golang.org/grpc/metadata"
"gopkg.in/macaroon-bakery.v2/bakery"
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
macaroon "gopkg.in/macaroon.v2"
@ -152,34 +150,31 @@ func (svc *Service) ValidateMacaroon(ctx context.Context,
requiredPermissions []bakery.Op, fullMethod string) error {
// Get macaroon bytes from context and unmarshal into macaroon.
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return fmt.Errorf("unable to get metadata from context")
macHex, err := RawMacaroonFromContext(ctx)
if err != nil {
return err
}
if len(md["macaroon"]) != 1 {
return fmt.Errorf("expected 1 macaroon, got %d",
len(md["macaroon"]))
// With the macaroon obtained, we'll now decode the hex-string encoding.
macBytes, err := hex.DecodeString(macHex)
if err != nil {
return err
}
return svc.CheckMacAuth(
ctx, md["macaroon"][0], requiredPermissions, fullMethod,
ctx, macBytes, requiredPermissions, fullMethod,
)
}
// CheckMacAuth checks that the macaroon is not disobeying any caveats and is
// authorized to perform the operation the user wants to perform.
func (svc *Service) CheckMacAuth(ctx context.Context, macStr string,
func (svc *Service) CheckMacAuth(ctx context.Context, macBytes []byte,
requiredPermissions []bakery.Op, fullMethod string) error {
// With the macaroon obtained, we'll now decode the hex-string
// encoding, then unmarshal it from binary into its concrete struct
// representation.
macBytes, err := hex.DecodeString(macStr)
if err != nil {
return err
}
// With the macaroon obtained, we'll now unmarshal it from binary into
// its concrete struct representation.
mac := &macaroon.Macaroon{}
err = mac.UnmarshalBinary(macBytes)
err := mac.UnmarshalBinary(macBytes)
if err != nil {
return err
}
@ -264,3 +259,37 @@ func (svc *Service) GenerateNewRootKey() error {
func (svc *Service) ChangePassword(oldPw, newPw []byte) error {
return svc.rks.ChangePassword(oldPw, newPw)
}
// RawMacaroonFromContext is a helper function that extracts a raw macaroon
// from the given incoming gRPC request context.
func RawMacaroonFromContext(ctx context.Context) (string, error) {
// Get macaroon bytes from context and unmarshal into macaroon.
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", fmt.Errorf("unable to get metadata from context")
}
if len(md["macaroon"]) != 1 {
return "", fmt.Errorf("expected 1 macaroon, got %d",
len(md["macaroon"]))
}
return md["macaroon"][0], nil
}
// SafeCopyMacaroon creates a copy of a macaroon that is safe to be used and
// modified. This is necessary because the macaroon library's own Clone() method
// is unsafe for certain edge cases, resulting in both the cloned and the
// original macaroons to be modified.
func SafeCopyMacaroon(mac *macaroon.Macaroon) (*macaroon.Macaroon, error) {
macBytes, err := mac.MarshalBinary()
if err != nil {
return nil, err
}
newMac := &macaroon.Macaroon{}
if err := newMac.UnmarshalBinary(macBytes); err != nil {
return nil, err
}
return newMac, nil
}

View file

@ -253,3 +253,58 @@ func TestDeleteMacaroonID(t *testing.T) {
ids, _ := service.ListMacaroonIDs(ctxb)
require.Equal(t, expectedIDs[1:], ids, "root key IDs mismatch")
}
// TestCloneMacaroons tests that macaroons can be cloned correctly and that
// modifications to the copy don't affect the original.
func TestCloneMacaroons(t *testing.T) {
// Get a configured version of the constraint function.
constraintFunc := macaroons.TimeoutConstraint(3)
// Now we need a dummy macaroon that we can apply the constraint
// function to.
testMacaroon := createDummyMacaroon(t)
err := constraintFunc(testMacaroon)
require.NoError(t, err)
// Check that the caveat has an empty location.
require.Equal(
t, "", testMacaroon.Caveats()[0].Location,
"expected caveat location to be empty, found: %s",
testMacaroon.Caveats()[0].Location,
)
// Make a copy of the macaroon.
newMacCred, err := macaroons.NewMacaroonCredential(testMacaroon)
require.NoError(t, err)
newMac := newMacCred.Macaroon
require.Equal(
t, "", newMac.Caveats()[0].Location,
"expected new caveat location to be empty, found: %s",
newMac.Caveats()[0].Location,
)
// They should be deep equal as well.
testMacaroonBytes, err := testMacaroon.MarshalBinary()
require.NoError(t, err)
newMacBytes, err := newMac.MarshalBinary()
require.NoError(t, err)
require.Equal(t, testMacaroonBytes, newMacBytes)
// Modify the caveat location on the old macaroon.
testMacaroon.Caveats()[0].Location = "mars"
// The old macaroon's caveat location should be changed.
require.Equal(
t, "mars", testMacaroon.Caveats()[0].Location,
"expected caveat location to be empty, found: %s",
testMacaroon.Caveats()[0].Location,
)
// The new macaroon's caveat location should stay untouched.
require.Equal(
t, "", newMac.Caveats()[0].Location,
"expected new caveat location to be empty, found: %s",
newMac.Caveats()[0].Location,
)
}

View file

@ -75,7 +75,12 @@ var (
"starting up, but not yet ready to accept calls")
// macaroonWhitelist defines methods that we don't require macaroons to
// access.
// access. We also allow these methods to be called even if not all
// mandatory middlewares are registered yet. If the wallet is locked
// then a middleware cannot register itself, creating an impossible
// situation. Also, a middleware might want to check the state of lnd
// by calling the State service before it registers itself. So we also
// need to exclude those calls from the mandatory middleware check.
macaroonWhitelist = map[string]struct{}{
// We allow all calls to the WalletUnlocker without macaroons.
"/lnrpc.WalletUnlocker/GenSeed": {},
@ -91,8 +96,43 @@ var (
)
// InterceptorChain is a struct that can be added to the running GRPC server,
// intercepting API calls. This is useful for logging, enforcing permissions
// etc.
// intercepting API calls. This is useful for logging, enforcing permissions,
// supporting middleware etc. The following diagram shows the order of each
// interceptor in the chain and when exactly requests/responses are intercepted
// and forwarded to external middleware for approval/modification. Middleware in
// general can only intercept gRPC requests/responses that are sent by the
// client with a macaroon that contains a custom caveat that is supported by one
// of the registered middlewares.
//
// |
// | gRPC request from client
// |
// +---v--------------------------------+
// | InterceptorChain |
// +-+----------------------------------+
// | Log Interceptor |
// +----------------------------------+
// | RPC State Interceptor |
// +----------------------------------+
// | Macaroon Interceptor |
// +----------------------------------+--------> +---------------------+
// | RPC Macaroon Middleware Handler |<-------- | External Middleware |
// +----------------------------------+ | - approve request |
// | Prometheus Interceptor | +---------------------+
// +-+--------------------------------+
// | validated gRPC request from client
// +---v--------------------------------+
// | main gRPC server |
// +---+--------------------------------+
// |
// | original gRPC request to client
// |
// +---v--------------------------------+--------> +---------------------+
// | RPC Macaroon Middleware Handler |<-------- | External Middleware |
// +---+--------------------------------+ | - modify response |
// | +---------------------+
// | edited gRPC request to client
// v
type InterceptorChain struct {
// Required by the grpc-gateway/v2 library for forward compatibility.
lnrpc.UnimplementedStateServer
@ -117,9 +157,22 @@ type InterceptorChain struct {
// permissionMap is the permissions to enforce if macaroons are used.
permissionMap map[string][]bakery.Op
// rpcsLog is the logger used to log calles to the RPCs intercepted.
// rpcsLog is the logger used to log calls to the RPCs intercepted.
rpcsLog btclog.Logger
// registeredMiddleware is a map of all macaroon permission based RPC
// middleware clients that are currently registered. The map is keyed
// by the middleware's name.
registeredMiddleware map[string]*MiddlewareHandler
// mandatoryMiddleware is a list of all middleware that is considered to
// be mandatory. If any of them is not registered then all RPC requests
// (except for the macaroon white listed methods and the middleware
// registration itself) are blocked. This is a security feature to make
// sure that requests can't just go through unobserved/unaudited if a
// middleware crashes.
mandatoryMiddleware []string
quit chan struct{}
sync.RWMutex
}
@ -129,14 +182,18 @@ type InterceptorChain struct {
var _ lnrpc.StateServer = (*InterceptorChain)(nil)
// NewInterceptorChain creates a new InterceptorChain.
func NewInterceptorChain(log btclog.Logger, noMacaroons bool) *InterceptorChain {
func NewInterceptorChain(log btclog.Logger, noMacaroons bool,
mandatoryMiddleware []string) *InterceptorChain {
return &InterceptorChain{
state: waitingToStart,
ntfnServer: subscribe.NewServer(),
noMacaroons: noMacaroons,
permissionMap: make(map[string][]bakery.Op),
rpcsLog: log,
quit: make(chan struct{}),
state: waitingToStart,
ntfnServer: subscribe.NewServer(),
noMacaroons: noMacaroons,
permissionMap: make(map[string][]bakery.Op),
rpcsLog: log,
registeredMiddleware: make(map[string]*MiddlewareHandler),
mandatoryMiddleware: mandatoryMiddleware,
quit: make(chan struct{}),
}
}
@ -241,7 +298,7 @@ func rpcStateToWalletState(state rpcState) (lnrpc.WalletState, error) {
// state will always be delivered immediately.
//
// NOTE: Part of the StateService interface.
func (r *InterceptorChain) SubscribeState(req *lnrpc.SubscribeStateRequest,
func (r *InterceptorChain) SubscribeState(_ *lnrpc.SubscribeStateRequest,
stream lnrpc.State_SubscribeStateServer) error {
sendStateUpdate := func(state rpcState) error {
@ -302,9 +359,9 @@ func (r *InterceptorChain) SubscribeState(req *lnrpc.SubscribeStateRequest,
}
}
// GetState returns he current wallet state.
// GetState returns the current wallet state.
func (r *InterceptorChain) GetState(_ context.Context,
req *lnrpc.GetStateRequest) (*lnrpc.GetStateResponse, error) {
_ *lnrpc.GetStateRequest) (*lnrpc.GetStateResponse, error) {
r.RLock()
state := r.state
@ -359,6 +416,78 @@ func (r *InterceptorChain) Permissions() map[string][]bakery.Op {
return c
}
// RegisterMiddleware registers a new middleware that will handle request/
// response interception for all RPC messages that are initiated with a custom
// macaroon caveat. The name of the custom caveat a middleware is handling is
// also its unique identifier. Only one middleware can be registered for each
// custom caveat.
func (r *InterceptorChain) RegisterMiddleware(mw *MiddlewareHandler) error {
r.Lock()
defer r.Unlock()
// The name of the middleware is the unique identifier.
registered, ok := r.registeredMiddleware[mw.middlewareName]
if ok {
return fmt.Errorf("a middleware with the name '%s' is already "+
"registered", registered.middlewareName)
}
// For now, we only want one middleware per custom caveat name. If we
// allowed multiple middlewares handling the same caveat there would be
// a need for extra call chaining logic, and they could overwrite each
// other's responses.
for name, middleware := range r.registeredMiddleware {
if middleware.customCaveatName == mw.customCaveatName {
return fmt.Errorf("a middleware is already registered "+
"for the custom caveat name '%s': %v",
mw.customCaveatName, name)
}
}
r.registeredMiddleware[mw.middlewareName] = mw
return nil
}
// RemoveMiddleware removes the middleware that handles the given custom caveat
// name.
func (r *InterceptorChain) RemoveMiddleware(middlewareName string) {
r.Lock()
defer r.Unlock()
log.Debugf("Removing middleware %s", middlewareName)
delete(r.registeredMiddleware, middlewareName)
}
// CustomCaveatSupported makes sure a middleware that handles the given custom
// caveat name is registered. If none is, an error is returned, signalling to
// the macaroon bakery and its validator to reject macaroons that have a custom
// caveat with that name.
//
// NOTE: This method is part of the macaroons.CustomCaveatAcceptor interface.
func (r *InterceptorChain) CustomCaveatSupported(customCaveatName string) error {
r.RLock()
defer r.RUnlock()
// We only accept requests with a custom caveat if we also have a
// middleware registered that handles that custom caveat. That is
// crucial for security! Otherwise a request with an encumbered (=has
// restricted permissions based upon the custom caveat condition)
// macaroon would not be validated against the limitations that the
// custom caveat implicate. Since the map is keyed by the _name_ of the
// middleware, we need to loop through all of them to see if one has
// the given custom macaroon caveat name.
for _, middleware := range r.registeredMiddleware {
if middleware.customCaveatName == customCaveatName {
return nil
}
}
return fmt.Errorf("cannot accept macaroon with custom caveat '%s', "+
"no middleware registered to handle it", customCaveatName)
}
// CreateServerOpts creates the GRPC server options that can be added to a GRPC
// server in order to add this InterceptorChain.
func (r *InterceptorChain) CreateServerOpts() []grpc.ServerOption {
@ -393,6 +522,15 @@ func (r *InterceptorChain) CreateServerOpts() []grpc.ServerOption {
strmInterceptors, r.MacaroonStreamServerInterceptor(),
)
// Next, we'll add the interceptors for our custom macaroon caveat based
// middleware.
unaryInterceptors = append(
unaryInterceptors, r.middlewareUnaryServerInterceptor(),
)
strmInterceptors = append(
strmInterceptors, r.middlewareStreamServerInterceptor(),
)
// Get interceptors for Prometheus to gather gRPC performance metrics.
// If monitoring is disabled, GetPromInterceptors() will return empty
// slices.
@ -614,3 +752,245 @@ func (r *InterceptorChain) rpcStateStreamServerInterceptor() grpc.StreamServerIn
return handler(srv, ss)
}
}
// middlewareUnaryServerInterceptor is a unary gRPC interceptor that intercepts
// all requests and responses that are sent with a macaroon containing a custom
// caveat condition that is handled by registered middleware.
func (r *InterceptorChain) middlewareUnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context,
req interface{}, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (interface{}, error) {
// Make sure we don't allow any requests through if one of the
// mandatory middlewares is missing.
fullMethod := info.FullMethod
if err := r.checkMandatoryMiddleware(fullMethod); err != nil {
return nil, err
}
msg, err := NewMessageInterceptionRequest(
ctx, TypeRequest, false, info.FullMethod, req,
)
if err != nil {
return nil, err
}
err = r.acceptRequest(msg)
if err != nil {
return nil, err
}
resp, respErr := handler(ctx, req)
if respErr != nil {
return resp, respErr
}
return r.interceptResponse(ctx, false, info.FullMethod, resp)
}
}
// middlewareStreamServerInterceptor is a streaming gRPC interceptor that
// intercepts all requests and responses that are sent with a macaroon
// containing a custom caveat condition that is handled by registered
// middleware.
func (r *InterceptorChain) middlewareStreamServerInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{},
ss grpc.ServerStream, info *grpc.StreamServerInfo,
handler grpc.StreamHandler) error {
// Don't intercept the interceptor itself which is a streaming
// RPC too!
fullMethod := info.FullMethod
if fullMethod == lnrpc.RegisterRPCMiddlewareURI {
return handler(srv, ss)
}
// Make sure we don't allow any requests through if one of the
// mandatory middlewares is missing. We add this check here to
// make sure the middleware registration itself can still be
// called.
if err := r.checkMandatoryMiddleware(fullMethod); err != nil {
return err
}
// To give the middleware a chance to accept or reject the
// establishment of the stream itself (and not only when the
// first message is sent on the stream), we send an intercept
// request for the stream auth now:
msg, err := NewStreamAuthInterceptionRequest(
ss.Context(), info.FullMethod,
)
if err != nil {
return err
}
err = r.acceptRequest(msg)
if err != nil {
return err
}
wrappedSS := &serverStreamWrapper{
ServerStream: ss,
fullMethod: info.FullMethod,
interceptor: r,
}
return handler(srv, wrappedSS)
}
}
// checkMandatoryMiddleware makes sure that each of the middlewares declared as
// mandatory is currently registered.
func (r *InterceptorChain) checkMandatoryMiddleware(fullMethod string) error {
r.RLock()
defer r.RUnlock()
// Allow calls that are whitelisted for macaroons as well, otherwise we
// get into impossible situations where the wallet is locked but the
// unlock call is denied because the middleware isn't registered. But
// the middleware cannot register itself because the wallet is locked.
if _, ok := macaroonWhitelist[fullMethod]; ok {
return nil
}
// Not a white listed call so make sure every mandatory middleware is
// currently connected to lnd.
for _, name := range r.mandatoryMiddleware {
if _, ok := r.registeredMiddleware[name]; !ok {
return fmt.Errorf("mandatory middleware '%s' is "+
"currently not registered, not allowing any "+
"RPC calls", name)
}
}
return nil
}
// acceptRequest sends an intercept request to all middlewares that have
// registered for it. This means either a middleware has requested read-only
// access or the request actually has a macaroon which a caveat the middleware
// registered for.
func (r *InterceptorChain) acceptRequest(msg *InterceptionRequest) error {
r.RLock()
defer r.RUnlock()
for _, middleware := range r.registeredMiddleware {
// If there is a custom caveat in the macaroon, make sure the
// middleware registered for it. Or if a middleware registered
// for read-only mode, it also gets the request.
hasCustomCaveat := macaroons.HasCustomCaveat(
msg.Macaroon, middleware.customCaveatName,
)
if !hasCustomCaveat && !middleware.readOnly {
continue
}
resp, err := middleware.intercept(msg)
// Error during interception itself.
if err != nil {
return err
}
// Error returned from middleware client.
if resp.err != nil {
return resp.err
}
}
return nil
}
// interceptResponse sends out an intercept request for an RPC response. Since
// middleware that hasn't registered for the read-only mode has the option to
// overwrite/replace the response, this needs to be handled differently than the
// request/auth path above.
func (r *InterceptorChain) interceptResponse(ctx context.Context,
isStream bool, fullMethod string, m interface{}) (interface{}, error) {
r.RLock()
defer r.RUnlock()
currentMessage := m
for _, middleware := range r.registeredMiddleware {
msg, err := NewMessageInterceptionRequest(
ctx, TypeResponse, isStream, fullMethod, currentMessage,
)
if err != nil {
return nil, err
}
// If there is a custom caveat in the macaroon, make sure the
// middleware registered for it. Or if a middleware registered
// for read-only mode, it also gets the request.
hasCustomCaveat := macaroons.HasCustomCaveat(
msg.Macaroon, middleware.customCaveatName,
)
if !hasCustomCaveat && !middleware.readOnly {
continue
}
resp, err := middleware.intercept(msg)
// Error during interception itself.
if err != nil {
return nil, err
}
// Error returned from middleware client.
if resp.err != nil {
return nil, resp.err
}
// The message was replaced, make sure the next middleware in
// line receives the updated message.
if !middleware.readOnly && resp.replace {
currentMessage = resp.replacement
}
}
return currentMessage, nil
}
// serverStreamWrapper is a struct that wraps a server stream in a way that all
// requests and responses can be intercepted individually.
type serverStreamWrapper struct {
// ServerStream is the stream that's being wrapped.
grpc.ServerStream
fullMethod string
interceptor *InterceptorChain
}
// SendMsg is called when lnd sends a message to the client. This is wrapped to
// intercept streaming RPC responses.
func (w *serverStreamWrapper) SendMsg(m interface{}) error {
newMsg, err := w.interceptor.interceptResponse(
w.ServerStream.Context(), true, w.fullMethod, m,
)
if err != nil {
return err
}
return w.ServerStream.SendMsg(newMsg)
}
// RecvMsg is called when lnd wants to receive a message from the client. This
// is wrapped to intercept streaming RPC requests.
func (w *serverStreamWrapper) RecvMsg(m interface{}) error {
err := w.ServerStream.RecvMsg(m)
if err != nil {
return err
}
msg, err := NewMessageInterceptionRequest(
w.ServerStream.Context(), TypeRequest, true, w.fullMethod,
m,
)
if err != nil {
return err
}
return w.interceptor.acceptRequest(msg)
}

32
rpcperms/log.go Normal file
View file

@ -0,0 +1,32 @@
package rpcperms
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// Subsystem defines the logging code for this subsystem.
const Subsystem = "RPCP"
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger(Subsystem, nil))
}
// DisableLog disables all library log output. Logging output is disabled
// by default until UseLogger is called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

View file

@ -0,0 +1,526 @@
package rpcperms
import (
"context"
"encoding/hex"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"gopkg.in/macaroon.v2"
)
var (
// ErrShuttingDown is the error that's returned when the server is
// shutting down and a request cannot be served anymore.
ErrShuttingDown = errors.New("server shutting down")
// ErrTimeoutReached is the error that's returned if any of the
// middleware's tasks is not completed in the given time.
ErrTimeoutReached = errors.New("intercept timeout reached")
// errClientQuit is the error that's returned if the client closes the
// middleware communication stream before a request was fully handled.
errClientQuit = errors.New("interceptor RPC client quit")
)
// MiddlewareHandler is a type that communicates with a middleware over the
// established bi-directional RPC stream. It sends messages to the middleware
// whenever the custom business logic implemented there should give feedback to
// a request or response that's happening on the main gRPC server.
type MiddlewareHandler struct {
// lastRequestID is the ID of the last request that was forwarded to the
// middleware.
//
// NOTE: Must be used atomically!
lastRequestID uint64
middlewareName string
readOnly bool
customCaveatName string
receive func() (*lnrpc.RPCMiddlewareResponse, error)
send func(request *lnrpc.RPCMiddlewareRequest) error
interceptRequests chan *interceptRequest
timeout time.Duration
// params are our current chain params.
params *chaincfg.Params
// done is closed when the rpc client terminates.
done chan struct{}
// quit is closed when lnd is shutting down.
quit chan struct{}
wg sync.WaitGroup
}
// NewMiddlewareHandler creates a new handler for the middleware with the given
// name and custom caveat name.
func NewMiddlewareHandler(name, customCaveatName string, readOnly bool,
receive func() (*lnrpc.RPCMiddlewareResponse, error),
send func(request *lnrpc.RPCMiddlewareRequest) error,
timeout time.Duration, params *chaincfg.Params,
quit chan struct{}) *MiddlewareHandler {
// We explicitly want to log this as a warning since intercepting any
// gRPC messages can also be used for malicious purposes and the user
// should be made aware of the risks.
log.Warnf("A new gRPC middleware with the name '%s' was registered "+
" with custom_macaroon_caveat='%s', read_only=%v. Make sure "+
"you trust the middleware author since that code will be able "+
"to intercept and possibly modify and gRPC messages sent/"+
"received to/from a client that has a macaroon with that "+
"custom caveat.", name, customCaveatName, readOnly)
return &MiddlewareHandler{
middlewareName: name,
customCaveatName: customCaveatName,
readOnly: readOnly,
receive: receive,
send: send,
interceptRequests: make(chan *interceptRequest),
timeout: timeout,
params: params,
done: make(chan struct{}),
quit: quit,
}
}
// intercept handles the full interception lifecycle of a single middleware
// event (stream authentication, request interception or response interception).
// The lifecycle consists of sending a message to the middleware, receiving a
// feedback on it and sending the feedback to the appropriate channel. All steps
// are guarded by the configured timeout to make sure a middleware cannot slow
// down requests too much.
func (h *MiddlewareHandler) intercept(
req *InterceptionRequest) (*interceptResponse, error) {
respChan := make(chan *interceptResponse, 1)
newRequest := &interceptRequest{
request: req,
response: respChan,
}
// timeout is the time after which intercept requests expire.
timeout := time.After(h.timeout)
// Send the request to the interceptRequests channel for the main
// goroutine to be picked up.
select {
case h.interceptRequests <- newRequest:
case <-timeout:
log.Errorf("MiddlewareHandler returned error - reached "+
"timeout of %v for request interception", h.timeout)
return nil, ErrTimeoutReached
case <-h.done:
return nil, errClientQuit
case <-h.quit:
return nil, ErrShuttingDown
}
// Receive the response and return it. If no response has been received
// in AcceptorTimeout, then return false.
select {
case resp := <-respChan:
return resp, nil
case <-timeout:
log.Errorf("MiddlewareHandler returned error - reached "+
"timeout of %v for response interception", h.timeout)
return nil, ErrTimeoutReached
case <-h.done:
return nil, errClientQuit
case <-h.quit:
return nil, ErrShuttingDown
}
}
// Run is the main loop for the middleware handler. This function will block
// until it receives the signal that lnd is shutting down, or the rpc stream is
// cancelled by the client.
func (h *MiddlewareHandler) Run() error {
// Wait for our goroutines to exit before we return.
defer h.wg.Wait()
defer log.Debugf("Exiting middleware run loop for %s", h.middlewareName)
// Create a channel that responses from middlewares are sent into.
responses := make(chan *lnrpc.RPCMiddlewareResponse)
// errChan is used by the receive loop to signal any errors that occur
// during reading from the stream. This is primarily used to shutdown
// the send loop in the case of an RPC client disconnecting.
errChan := make(chan error, 1)
// Start a goroutine to receive responses from the interceptor. We
// expect the receive function to block, so it must be run in a
// goroutine (otherwise we could not send more than one intercept
// request to the client).
h.wg.Add(1)
go func() {
h.receiveResponses(errChan, responses)
h.wg.Done()
}()
return h.sendInterceptRequests(errChan, responses)
}
// receiveResponses receives responses for our intercept requests and dispatches
// them into the responses channel provided, sending any errors that occur into
// the error channel provided.
func (h *MiddlewareHandler) receiveResponses(errChan chan error,
responses chan *lnrpc.RPCMiddlewareResponse) {
for {
resp, err := h.receive()
if err != nil {
errChan <- err
return
}
select {
case responses <- resp:
case <-h.done:
return
case <-h.quit:
return
}
}
}
// sendInterceptRequests handles intercept requests sent to us by our Accept()
// function, dispatching them to our acceptor stream and coordinating return of
// responses to their callers.
func (h *MiddlewareHandler) sendInterceptRequests(errChan chan error,
responses chan *lnrpc.RPCMiddlewareResponse) error {
// Close the done channel to indicate that the interceptor is no longer
// listening and any in-progress requests should be terminated.
defer close(h.done)
interceptRequests := make(map[uint64]*interceptRequest)
for {
select {
// Consume requests passed to us from our Accept() function and
// send them into our stream.
case newRequest := <-h.interceptRequests:
id := atomic.AddUint64(&h.lastRequestID, 1)
req := newRequest.request
interceptRequests[id] = newRequest
interceptReq, err := req.ToRPC(id)
if err != nil {
return err
}
if err := h.send(interceptReq); err != nil {
return err
}
// Process newly received responses from our interceptor,
// looking the original request up in our map of requests and
// dispatching the response.
case resp := <-responses:
requestInfo, ok := interceptRequests[resp.RequestId]
if !ok {
continue
}
response := &interceptResponse{}
switch msg := resp.GetMiddlewareMessage().(type) {
case *lnrpc.RPCMiddlewareResponse_Feedback:
t := msg.Feedback
if t.Error != "" {
response.err = fmt.Errorf("%s", t.Error)
break
}
// For intercepted responses we also allow the
// content itself to be overwritten.
if requestInfo.request.Type == TypeResponse &&
t.ReplaceResponse {
response.replace = true
protoMsg, err := parseProto(
requestInfo.request.ProtoTypeName,
t.ReplacementSerialized,
)
if err != nil {
response.err = err
break
}
response.replacement = protoMsg
}
default:
return fmt.Errorf("unknown middleware "+
"message: %v", msg)
}
select {
case requestInfo.response <- response:
case <-h.quit:
}
delete(interceptRequests, resp.RequestId)
// If we failed to receive from our middleware, we exit.
case err := <-errChan:
log.Errorf("Received an error: %v, shutting down", err)
return err
// Exit if we are shutting down.
case <-h.quit:
return ErrShuttingDown
}
}
}
// InterceptType defines the different types of intercept messages a middleware
// can receive.
type InterceptType uint8
const (
// TypeStreamAuth is the type of intercept message that is sent when a
// client or streaming RPC is initialized. A message with this type will
// be sent out during stream initialization so a middleware can
// accept/deny the whole stream instead of only single messages on the
// stream.
TypeStreamAuth InterceptType = 1
// TypeRequest is the type of intercept message that is sent when an RPC
// request message is sent to lnd. For client-streaming RPCs a new
// message of this type is sent for each individual RPC request sent to
// the stream.
TypeRequest InterceptType = 2
// TypeResponse is the type of intercept message that is sent when an
// RPC response message is sent from lnd to a client. For
// server-streaming RPCs a new message of this type is sent for each
// individual RPC response sent to the stream. Middleware has the option
// to modify a response message before it is sent out to the client.
TypeResponse InterceptType = 3
)
// InterceptionRequest is a struct holding all information that is sent to a
// middleware whenever there is something to intercept (auth, request,
// response).
type InterceptionRequest struct {
// Type is the type of the interception message.
Type InterceptType
// StreamRPC is set to true if the invoked RPC method is client or
// server streaming.
StreamRPC bool
// Macaroon holds the macaroon that the client sent to lnd.
Macaroon *macaroon.Macaroon
// RawMacaroon holds the raw binary serialized macaroon that the client
// sent to lnd.
RawMacaroon []byte
// CustomCaveatName is the name of the custom caveat that the middleware
// was intercepting for.
CustomCaveatName string
// CustomCaveatCondition is the condition of the custom caveat that the
// middleware was intercepting for. This can be empty for custom caveats
// that only have a name (marker caveats).
CustomCaveatCondition string
// FullURI is the full RPC method URI that was invoked.
FullURI string
// ProtoSerialized is the full request or response object in the
// protobuf binary serialization format.
ProtoSerialized []byte
// ProtoTypeName is the fully qualified name of the protobuf type of the
// request or response message that is serialized in the field above.
ProtoTypeName string
}
// NewMessageInterceptionRequest creates a new interception request for either
// a request or response message.
func NewMessageInterceptionRequest(ctx context.Context,
authType InterceptType, isStream bool, fullMethod string,
m interface{}) (*InterceptionRequest, error) {
mac, rawMacaroon, err := macaroonFromContext(ctx)
if err != nil {
return nil, err
}
rpcReq, ok := m.(proto.Message)
if !ok {
return nil, fmt.Errorf("msg is not proto message: %v", m)
}
rawRequest, err := proto.Marshal(rpcReq)
if err != nil {
return nil, fmt.Errorf("cannot marshal proto msg: %v", err)
}
return &InterceptionRequest{
Type: authType,
StreamRPC: isStream,
Macaroon: mac,
RawMacaroon: rawMacaroon,
FullURI: fullMethod,
ProtoSerialized: rawRequest,
ProtoTypeName: string(proto.MessageName(rpcReq)),
}, nil
}
// NewStreamAuthInterceptionRequest creates a new interception request for a
// stream authentication message.
func NewStreamAuthInterceptionRequest(ctx context.Context,
fullMethod string) (*InterceptionRequest, error) {
mac, rawMacaroon, err := macaroonFromContext(ctx)
if err != nil {
return nil, err
}
return &InterceptionRequest{
Type: TypeStreamAuth,
StreamRPC: true,
Macaroon: mac,
RawMacaroon: rawMacaroon,
FullURI: fullMethod,
}, nil
}
// macaroonFromContext tries to extract the macaroon from the incoming context.
// If there is no macaroon, a nil error is returned since some RPCs might not
// require a macaroon. But in case there is something in the macaroon header
// field that cannot be parsed, a non-nil error is returned.
func macaroonFromContext(ctx context.Context) (*macaroon.Macaroon, []byte,
error) {
macHex, err := macaroons.RawMacaroonFromContext(ctx)
if err != nil {
// If there is no macaroon, we continue anyway as it might be an
// RPC that doesn't require a macaroon.
return nil, nil, nil
}
macBytes, err := hex.DecodeString(macHex)
if err != nil {
return nil, nil, err
}
mac := &macaroon.Macaroon{}
if err := mac.UnmarshalBinary(macBytes); err != nil {
return nil, nil, err
}
return mac, macBytes, nil
}
// ToRPC converts the interception request to its RPC counterpart.
func (r *InterceptionRequest) ToRPC(id uint64) (*lnrpc.RPCMiddlewareRequest,
error) {
rpcRequest := &lnrpc.RPCMiddlewareRequest{
RequestId: id,
RawMacaroon: r.RawMacaroon,
CustomCaveatCondition: r.CustomCaveatCondition,
}
switch r.Type {
case TypeStreamAuth:
rpcRequest.InterceptType = &lnrpc.RPCMiddlewareRequest_StreamAuth{
StreamAuth: &lnrpc.StreamAuth{
MethodFullUri: r.FullURI,
},
}
case TypeRequest:
rpcRequest.InterceptType = &lnrpc.RPCMiddlewareRequest_Request{
Request: &lnrpc.RPCMessage{
MethodFullUri: r.FullURI,
StreamRpc: r.StreamRPC,
TypeName: r.ProtoTypeName,
Serialized: r.ProtoSerialized,
},
}
case TypeResponse:
rpcRequest.InterceptType = &lnrpc.RPCMiddlewareRequest_Response{
Response: &lnrpc.RPCMessage{
MethodFullUri: r.FullURI,
StreamRpc: r.StreamRPC,
TypeName: r.ProtoTypeName,
Serialized: r.ProtoSerialized,
},
}
default:
return nil, fmt.Errorf("unknown intercept type %v", r.Type)
}
return rpcRequest, nil
}
// interceptRequest is a struct that keeps track of an interception request sent
// out to a middleware and the response that is eventually sent back by the
// middleware.
type interceptRequest struct {
request *InterceptionRequest
response chan *interceptResponse
}
// interceptResponse is the response a middleware sends back for each
// intercepted message.
type interceptResponse struct {
err error
replace bool
replacement interface{}
}
// parseProto parses a proto serialized message of the given type into its
// native version.
func parseProto(typeName string, serialized []byte) (proto.Message, error) {
messageType, err := protoregistry.GlobalTypes.FindMessageByName(
protoreflect.FullName(typeName),
)
if err != nil {
return nil, err
}
msg := messageType.New()
err = proto.Unmarshal(serialized, msg.Interface())
if err != nil {
return nil, err
}
return msg.Interface(), nil
}

View file

@ -528,6 +528,10 @@ func MainRPCServerPermissions() map[string][]bakery.Op {
Entity: "offchain",
Action: "write",
}},
lnrpc.RegisterRPCMiddlewareURI: {{
Entity: "macaroon",
Action: "write",
}},
}
}
@ -6995,8 +6999,7 @@ func (r *rpcServer) CheckMacaroonPermissions(ctx context.Context,
}
err := r.macService.CheckMacAuth(
ctx, hex.EncodeToString(req.Macaroon), permissions,
req.FullMethod,
ctx, req.Macaroon, permissions, req.FullMethod,
)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
@ -7164,3 +7167,105 @@ func (r *rpcServer) FundingStateStep(ctx context.Context,
// current state?
return &lnrpc.FundingStateStepResp{}, nil
}
// RegisterRPCMiddleware adds a new gRPC middleware to the interceptor chain. A
// gRPC middleware is software component external to lnd that aims to add
// additional business logic to lnd by observing/intercepting/validating
// incoming gRPC client requests and (if needed) replacing/overwriting outgoing
// messages before they're sent to the client. When registering the middleware
// must identify itself and indicate what custom macaroon caveats it wants to
// be responsible for. Only requests that contain a macaroon with that specific
// custom caveat are then sent to the middleware for inspection. As a security
// measure, _no_ middleware can intercept requests made with _unencumbered_
// macaroons!
func (r *rpcServer) RegisterRPCMiddleware(
stream lnrpc.Lightning_RegisterRPCMiddlewareServer) error {
// This is a security critical functionality and needs to be enabled
// specifically by the user.
if !r.cfg.RPCMiddleware.Enable {
return fmt.Errorf("RPC middleware not enabled in config")
}
// When registering a middleware the first message being sent from the
// middleware must be a registration message containing its name and the
// custom caveat it wants to register for.
var (
registerChan = make(chan *lnrpc.MiddlewareRegistration, 1)
errChan = make(chan error, 1)
)
ctxc, cancel := context.WithTimeout(
stream.Context(), r.cfg.RPCMiddleware.InterceptTimeout,
)
defer cancel()
// Read the first message in a goroutine because the Recv method blocks
// until the message arrives.
go func() {
msg, err := stream.Recv()
if err != nil {
errChan <- err
return
}
registerChan <- msg.GetRegister()
}()
// Wait for the initial message to arrive or time out if it takes too
// long.
var registerMsg *lnrpc.MiddlewareRegistration
select {
case registerMsg = <-registerChan:
if registerMsg == nil {
return fmt.Errorf("invalid initial middleware " +
"registration message")
}
case err := <-errChan:
return fmt.Errorf("error receiving initial middleware "+
"registration message: %v", err)
case <-ctxc.Done():
return ctxc.Err()
case <-r.quit:
return ErrServerShuttingDown
}
// Make sure the registration is valid.
const nameMinLength = 5
if len(registerMsg.MiddlewareName) < nameMinLength {
return fmt.Errorf("invalid middleware name, use descriptive "+
"name of at least %d characters", nameMinLength)
}
readOnly := registerMsg.ReadOnlyMode
caveatName := registerMsg.CustomMacaroonCaveatName
switch {
case readOnly && len(caveatName) > 0:
return fmt.Errorf("cannot set read-only and custom caveat " +
"name at the same time")
case !readOnly && len(caveatName) < nameMinLength:
return fmt.Errorf("need to set either custom caveat name "+
"of at least %d characters or read-only mode",
nameMinLength)
}
middleware := rpcperms.NewMiddlewareHandler(
registerMsg.MiddlewareName,
caveatName, readOnly, stream.Recv, stream.Send,
r.cfg.RPCMiddleware.InterceptTimeout,
r.cfg.ActiveNetParams.Params, r.quit,
)
// Add the RPC middleware to the interceptor chain and defer its
// removal.
if err := r.interceptorChain.RegisterMiddleware(middleware); err != nil {
return fmt.Errorf("error registering middleware: %v", err)
}
defer r.interceptorChain.RemoveMiddleware(registerMsg.MiddlewareName)
return middleware.Run()
}

View file

@ -1151,6 +1151,20 @@ litecoin.node=ltcd
; Defaults to the hostname.
; cluster.id=example.com
[rpcmiddleware]
; Enable the RPC middleware interceptor functionality.
; rpcmiddleware.enable=true
; Time after which a RPC middleware intercept request will time out and return
; an error if it hasn't yet received a response.
; rpcmiddleware.intercepttimeout=2s
; Add the named middleware to the list of mandatory middlewares. All RPC
; requests are blocked/denied if any of the mandatory middlewares is not
; registered. Can be specified multiple times.
; rpcmiddleware.addmandatory=my-example-middleware
; rpcmiddleware.addmandatory=other-mandatory-middleware
[bolt]