Merge pull request #281 from guggero/macaroons

[2/3] loopd: add macaroon authentication to the loop RPC server
pull/294/head
Oliver Gugger 4 years ago committed by GitHub
commit d638d07fe3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -333,18 +333,25 @@ pending swaps after a restart.
Information about pending swaps is stored persistently in the swap database. Information about pending swaps is stored persistently in the swap database.
Its location is `~/.loopd/<network>/loop.db`. Its location is `~/.loopd/<network>/loop.db`.
## Transport security ## Authentication and transport security
The gRPC and REST connections of `loopd` are encrypted with TLS the same way The gRPC and REST connections of `loopd` are encrypted with TLS and secured with
`lnd` is. macaroon authentication the same way `lnd` is.
If no custom loop directory is set then the TLS certificate is stored in If no custom loop directory is set then the TLS certificate is stored in
`~/.loopd/<network>/tls.cert`. `~/.loop/<network>/tls.cert` and the base macaroon in
`~/.loop/<network>/loop.macaroon`.
The `loop` command will pick up the file automatically on mainnet if no custom The `loop` command will pick up these file automatically on mainnet if no custom
loop directory is used. For other networks it should be sufficient to add the loop directory is used. For other networks it should be sufficient to add the
`--network` flag to tell the CLI in what sub directory to look for the files. `--network` flag to tell the CLI in what sub directory to look for the files.
For more information on macaroons,
[see the macaroon documentation of lnd.](https://github.com/lightningnetwork/lnd/blob/master/docs/macaroons.md)
**NOTE**: Loop's macaroons are independent from `lnd`'s. The same macaroon
cannot be used for both `loopd` and `lnd`.
## Multiple Simultaneous Swaps ## Multiple Simultaneous Swaps
It is possible to execute multiple swaps simultaneously. Just keep loopd It is possible to execute multiple swaps simultaneously. Just keep loopd

@ -66,9 +66,9 @@ var (
Value: loopd.DefaultTLSCertPath, Value: loopd.DefaultTLSCertPath,
} }
macaroonPathFlag = cli.StringFlag{ macaroonPathFlag = cli.StringFlag{
Name: "macaroonpath", Name: "macaroonpath",
Usage: "path to macaroon file, only needed if loop runs " + Usage: "path to macaroon file",
"in the same process as lnd", Value: loopd.DefaultMacaroonPath,
} }
) )
@ -171,25 +171,31 @@ func extractPathArgs(ctx *cli.Context) (string, string, error) {
} }
// We'll now fetch the loopdir so we can make a decision on how to // We'll now fetch the loopdir so we can make a decision on how to
// properly read the cert. This will either be the default, or will have // properly read the macaroons and also the cert. This will either be
// been overwritten by the end user. // the default, or will have been overwritten by the end user.
loopDir := lncfg.CleanAndExpandPath(ctx.GlobalString(loopDirFlag.Name)) loopDir := lncfg.CleanAndExpandPath(ctx.GlobalString(loopDirFlag.Name))
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString( tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
tlsCertFlag.Name, tlsCertFlag.Name,
)) ))
macPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
macaroonPathFlag.Name,
))
// If a custom lnd directory was set, we'll also check if custom paths // If a custom loop directory was set, we'll also check if custom paths
// for the TLS cert file were set as well. If not, we'll override their // for the TLS cert and macaroon file were set as well. If not, we'll
// paths so they can be found within the custom loop directory set. This // override their paths so they can be found within the custom loop
// allows us to set a custom lnd directory, along with custom paths to // directory set. This allows us to set a custom loop directory, along
// the TLS cert file. // with custom paths to the TLS cert and macaroon file.
if loopDir != loopd.LoopDirBase || networkStr != loopd.DefaultNetwork { if loopDir != loopd.LoopDirBase || networkStr != loopd.DefaultNetwork {
tlsCertPath = filepath.Join( tlsCertPath = filepath.Join(
loopDir, networkStr, loopd.DefaultTLSCertFilename, loopDir, networkStr, loopd.DefaultTLSCertFilename,
) )
macPath = filepath.Join(
loopDir, networkStr, loopd.DefaultMacaroonFilename,
)
} }
return tlsCertPath, ctx.GlobalString(macaroonPathFlag.Name), nil return tlsCertPath, macPath, nil
} }
type inLimits struct { type inLimits struct {
@ -373,8 +379,15 @@ func logSwap(swap *looprpc.SwapStatus) {
func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn, func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn,
error) { error) {
// We always need to send a macaroon.
macOption, err := readMacaroon(macaroonPath)
if err != nil {
return nil, err
}
opts := []grpc.DialOption{ opts := []grpc.DialOption{
grpc.WithDefaultCallOptions(maxMsgRecvSize), grpc.WithDefaultCallOptions(maxMsgRecvSize),
macOption,
} }
// TLS cannot be disabled, we'll always have a cert file to read. // TLS cannot be disabled, we'll always have a cert file to read.
@ -383,15 +396,6 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn,
return nil, err return nil, err
} }
// Macaroons are not yet enabled by default.
if macaroonPath != "" {
macOption, err := readMacaroon(macaroonPath)
if err != nil {
return nil, err
}
opts = append(opts, macOption)
}
opts = append(opts, grpc.WithTransportCredentials(creds)) opts = append(opts, grpc.WithTransportCredentials(creds))
conn, err := grpc.Dial(address, opts...) conn, err := grpc.Dial(address, opts...)

@ -12,7 +12,10 @@ require (
github.com/jessevdk/go-flags v1.4.0 github.com/jessevdk/go-flags v1.4.0
github.com/lightninglabs/lndclient v0.11.0-0 github.com/lightninglabs/lndclient v0.11.0-0
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d
github.com/lightningnetwork/lnd v0.11.0-beta
// TODO(guggero): Bump lnd to the final v0.11.1-beta version once it's
// released.
github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20200911014924-bc6e52888763
github.com/lightningnetwork/lnd/cert v1.0.3 github.com/lightningnetwork/lnd/cert v1.0.3
github.com/lightningnetwork/lnd/queue v1.0.4 github.com/lightningnetwork/lnd/queue v1.0.4
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
@ -20,6 +23,7 @@ require (
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0 golang.org/x/net v0.0.0-20191002035440-2ec189313ef0
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c
google.golang.org/grpc v1.24.0 google.golang.org/grpc v1.24.0
gopkg.in/macaroon-bakery.v2 v2.0.1
gopkg.in/macaroon.v2 v2.1.0 gopkg.in/macaroon.v2 v2.1.0
) )

@ -45,6 +45,8 @@ github.com/btcsuite/btcwallet/walletdb v1.3.3 h1:u6e7vRIKBF++cJy+hOHaMGg+88ZTwvp
github.com/btcsuite/btcwallet/walletdb v1.3.3/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= github.com/btcsuite/btcwallet/walletdb v1.3.3/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY= github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY=
github.com/btcsuite/btcwallet/wtxmgr v1.2.0 h1:ZUYPsSv8GjF9KK7lboB2OVHF0uYEcHxgrCfFWqPd9NA= github.com/btcsuite/btcwallet/wtxmgr v1.2.0 h1:ZUYPsSv8GjF9KK7lboB2OVHF0uYEcHxgrCfFWqPd9NA=
github.com/btcsuite/btcwallet/wtxmgr v1.2.0 h1:ZUYPsSv8GjF9KK7lboB2OVHF0uYEcHxgrCfFWqPd9NA=
github.com/btcsuite/btcwallet/wtxmgr v1.2.0/go.mod h1:h8hkcKUE3X7lMPzTUoGnNiw5g7VhGrKEW3KpR2r0VnY=
github.com/btcsuite/btcwallet/wtxmgr v1.2.0/go.mod h1:h8hkcKUE3X7lMPzTUoGnNiw5g7VhGrKEW3KpR2r0VnY= github.com/btcsuite/btcwallet/wtxmgr v1.2.0/go.mod h1:h8hkcKUE3X7lMPzTUoGnNiw5g7VhGrKEW3KpR2r0VnY=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
@ -179,14 +181,14 @@ github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce7
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI= github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI=
github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea h1:oCj48NQ8u7Vz+MmzHqt0db6mxcFZo3Ho7M5gCJauY/k= github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea h1:oCj48NQ8u7Vz+MmzHqt0db6mxcFZo3Ho7M5gCJauY/k=
github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4= github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4=
github.com/lightningnetwork/lnd v0.11.0-beta h1:pUAT7FMHqS+iarNxyRtgj96XKCGAWwmb6ZdiUBy78ts=
github.com/lightningnetwork/lnd v0.11.0-beta/go.mod h1:CzArvT7NFDLhVyW06+NJWSuWFmE6Ea+AjjA3txUBqTM= github.com/lightningnetwork/lnd v0.11.0-beta/go.mod h1:CzArvT7NFDLhVyW06+NJWSuWFmE6Ea+AjjA3txUBqTM=
github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20200911014924-bc6e52888763 h1:OUWOTo2BAcsnEaMQIf4gLktU3zGytx6pXrmjUNpZpdg=
github.com/lightningnetwork/lnd v0.11.0-beta.rc4.0.20200911014924-bc6e52888763/go.mod h1:IvrqVCc5tN2on6E7IHhrwyiM7FCHZ92LphZD+v88LXY=
github.com/lightningnetwork/lnd/cert v1.0.2/go.mod h1:fmtemlSMf5t4hsQmcprSoOykypAPp+9c+0d0iqTScMo= github.com/lightningnetwork/lnd/cert v1.0.2/go.mod h1:fmtemlSMf5t4hsQmcprSoOykypAPp+9c+0d0iqTScMo=
github.com/lightningnetwork/lnd/cert v1.0.3 h1:/K2gjzLgVI8we2IIPKc0ztWTEa85uds5sWXi1K6mOT0= github.com/lightningnetwork/lnd/cert v1.0.3 h1:/K2gjzLgVI8we2IIPKc0ztWTEa85uds5sWXi1K6mOT0=
github.com/lightningnetwork/lnd/cert v1.0.3/go.mod h1:3MWXVLLPI0Mg0XETm9fT4N9Vyy/8qQLmaM5589bEggM= github.com/lightningnetwork/lnd/cert v1.0.3/go.mod h1:3MWXVLLPI0Mg0XETm9fT4N9Vyy/8qQLmaM5589bEggM=
github.com/lightningnetwork/lnd/clock v1.0.1 h1:QQod8+m3KgqHdvVMV+2DRNNZS1GRFir8mHZYA+Z2hFo= github.com/lightningnetwork/lnd/clock v1.0.1 h1:QQod8+m3KgqHdvVMV+2DRNNZS1GRFir8mHZYA+Z2hFo=
github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg=
github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0=
github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms= github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms=
github.com/lightningnetwork/lnd/queue v1.0.4 h1:8Dq3vxAFSACPy+pKN88oPFhuCpCoAAChPBwa4BJxH4k= github.com/lightningnetwork/lnd/queue v1.0.4 h1:8Dq3vxAFSACPy+pKN88oPFhuCpCoAAChPBwa4BJxH4k=
github.com/lightningnetwork/lnd/queue v1.0.4/go.mod h1:YTkTVZCxz8tAYreH27EO3s8572ODumWrNdYW2E/YKxg= github.com/lightningnetwork/lnd/queue v1.0.4/go.mod h1:YTkTVZCxz8tAYreH27EO3s8572ODumWrNdYW2E/YKxg=
@ -246,7 +248,6 @@ github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@ -259,7 +260,6 @@ github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50 h1:ASw9n1EHMftwnP3Az4XW6e308+gNsrHzmdhd0Olz9Hs= go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50 h1:ASw9n1EHMftwnP3Az4XW6e308+gNsrHzmdhd0Olz9Hs=
go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=

@ -57,6 +57,16 @@ var (
DefaultTLSKeyPath = filepath.Join( DefaultTLSKeyPath = filepath.Join(
LoopDirBase, DefaultNetwork, DefaultTLSKeyFilename, LoopDirBase, DefaultNetwork, DefaultTLSKeyFilename,
) )
// DefaultMacaroonFilename is the default file name for the
// autogenerated loop macaroon.
DefaultMacaroonFilename = "loop.macaroon"
// DefaultMacaroonPath is the default full path of the base loop
// macaroon.
DefaultMacaroonPath = filepath.Join(
LoopDirBase, DefaultNetwork, DefaultMacaroonFilename,
)
) )
type lndConfig struct { type lndConfig struct {
@ -82,7 +92,7 @@ type Config struct {
RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"` RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"`
CORSOrigin string `long:"corsorigin" description:"The value to send in the Access-Control-Allow-Origin header. Header will be omitted if empty."` CORSOrigin string `long:"corsorigin" description:"The value to send in the Access-Control-Allow-Origin header. Header will be omitted if empty."`
LoopDir string `long:"loopdir" description:"The directory for all of loop's data. If set, this option overwrites --datadir, --logdir, --tlscertpath and --tlskeypath."` LoopDir string `long:"loopdir" description:"The directory for all of loop's data. If set, this option overwrites --datadir, --logdir, --tlscertpath, --tlskeypath and --macaroonpath."`
ConfigFile string `long:"configfile" description:"Path to configuration file."` ConfigFile string `long:"configfile" description:"Path to configuration file."`
DataDir string `long:"datadir" description:"Directory for loopdb."` DataDir string `long:"datadir" description:"Directory for loopdb."`
@ -93,6 +103,8 @@ type Config struct {
TLSAutoRefresh bool `long:"tlsautorefresh" description:"Re-generate TLS certificate and key if the IPs or domains are changed."` TLSAutoRefresh bool `long:"tlsautorefresh" description:"Re-generate TLS certificate and key if the IPs or domains are changed."`
TLSDisableAutofill bool `long:"tlsdisableautofill" description:"Do not include the interface IPs or the system hostname in TLS certificate, use first --tlsextradomain as Common Name instead, if set."` TLSDisableAutofill bool `long:"tlsdisableautofill" description:"Do not include the interface IPs or the system hostname in TLS certificate, use first --tlsextradomain as Common Name instead, if set."`
MacaroonPath string `long:"macaroonpath" description:"Path to write the macaroon for loop's RPC and REST services if it doesn't exist."`
LogDir string `long:"logdir" description:"Directory to log output."` LogDir string `long:"logdir" description:"Directory to log output."`
MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)."` MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)."`
MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB."` MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB."`
@ -133,6 +145,7 @@ func DefaultConfig() Config {
DebugLevel: defaultLogLevel, DebugLevel: defaultLogLevel,
TLSCertPath: DefaultTLSCertPath, TLSCertPath: DefaultTLSCertPath,
TLSKeyPath: DefaultTLSKeyPath, TLSKeyPath: DefaultTLSKeyPath,
MacaroonPath: DefaultMacaroonPath,
MaxLSATCost: lsat.DefaultMaxCostSats, MaxLSATCost: lsat.DefaultMaxCostSats,
MaxLSATFee: lsat.DefaultMaxRoutingFeeSats, MaxLSATFee: lsat.DefaultMaxRoutingFeeSats,
LoopOutMaxParts: defaultLoopOutMaxParts, LoopOutMaxParts: defaultLoopOutMaxParts,
@ -193,9 +206,9 @@ func Validate(cfg *Config) error {
cfg.DataDir = filepath.Join(cfg.DataDir, cfg.Network) cfg.DataDir = filepath.Join(cfg.DataDir, cfg.Network)
cfg.LogDir = filepath.Join(cfg.LogDir, cfg.Network) cfg.LogDir = filepath.Join(cfg.LogDir, cfg.Network)
// We want the TLS files to also be in the "namespaced" sub directory. // We want the TLS and macaroon files to also be in the "namespaced" sub
// Replace the default values with actual values in case the user // directory. Replace the default values with actual values in case the
// specified either loopdir or datadir. // user specified either loopdir or datadir.
if cfg.TLSCertPath == DefaultTLSCertPath { if cfg.TLSCertPath == DefaultTLSCertPath {
cfg.TLSCertPath = filepath.Join( cfg.TLSCertPath = filepath.Join(
cfg.DataDir, DefaultTLSCertFilename, cfg.DataDir, DefaultTLSCertFilename,
@ -206,6 +219,11 @@ func Validate(cfg *Config) error {
cfg.DataDir, DefaultTLSKeyFilename, cfg.DataDir, DefaultTLSKeyFilename,
) )
} }
if cfg.MacaroonPath == DefaultMacaroonPath {
cfg.MacaroonPath = filepath.Join(
cfg.DataDir, DefaultMacaroonFilename,
)
}
// If either of these directories do not exist, create them. // If either of these directories do not exist, create them.
if err := os.MkdirAll(cfg.DataDir, os.ModePerm); err != nil { if err := os.MkdirAll(cfg.DataDir, os.ModePerm); err != nil {

@ -16,7 +16,9 @@ import (
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/loop/looprpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc" "google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery"
) )
var ( var (
@ -79,6 +81,8 @@ type Daemon struct {
restServer *http.Server restServer *http.Server
restListener net.Listener restListener net.Listener
restCtxCancel func() restCtxCancel func()
macaroonService *macaroons.Service
} }
// New creates a new instance of the loop client daemon. // New creates a new instance of the loop client daemon.
@ -167,13 +171,29 @@ func (d *Daemon) StartAsSubserver(lndGrpc *lndclient.GrpcLndServices) error {
return d.initialize() return d.initialize()
} }
// ValidateMacaroon extracts the macaroon from the context's gRPC metadata,
// checks its signature, makes sure all specified permissions for the called
// method are contained within and finally ensures all caveat conditions are
// met. A non-nil error is returned if any of the checks fail. This method is
// needed to enable loopd running as an external subserver in the same process
// as lnd but still validate its own macaroons.
func (d *Daemon) ValidateMacaroon(ctx context.Context,
requiredPermissions []bakery.Op, fullMethod string) error {
// Delegate the call to loop's own macaroon validator service.
return d.macaroonService.ValidateMacaroon(
ctx, requiredPermissions, fullMethod,
)
}
// startWebServers starts the gRPC and REST servers in goroutines. // startWebServers starts the gRPC and REST servers in goroutines.
func (d *Daemon) startWebServers() error { func (d *Daemon) startWebServers() error {
var err error var err error
// With our client created, let's now finish setting up and start our // With our client created, let's now finish setting up and start our
// RPC server. // RPC server. First we add the security interceptor to our gRPC server
serverOpts := []grpc.ServerOption{} // options that checks the macaroons for validity.
serverOpts := d.macaroonInterceptor()
d.grpcServer = grpc.NewServer(serverOpts...) d.grpcServer = grpc.NewServer(serverOpts...)
looprpc.RegisterSwapClientServer(d.grpcServer, d) looprpc.RegisterSwapClientServer(d.grpcServer, d)
@ -322,6 +342,17 @@ func (d *Daemon) initialize() error {
// stop on main context cancel. So we create it early and pass it down. // stop on main context cancel. So we create it early and pass it down.
d.mainCtx, d.mainCtxCancel = context.WithCancel(context.Background()) d.mainCtx, d.mainCtxCancel = context.WithCancel(context.Background())
// Start the macaroon service and let it create its default macaroon in
// case it doesn't exist yet.
err = d.startMacaroonService()
if err != nil {
// The client is the only thing we started yet, so if we clean
// up its connection now, nothing else needs to be shut down at
// this point.
clientCleanup()
return err
}
// Now finally fully initialize the swap client RPC server instance. // Now finally fully initialize the swap client RPC server instance.
d.swapClientServer = swapClientServer{ d.swapClientServer = swapClientServer{
impl: swapclient, impl: swapclient,
@ -336,9 +367,13 @@ func (d *Daemon) initialize() error {
// Retrieve all currently existing swaps from the database. // Retrieve all currently existing swaps from the database.
swapsList, err := d.impl.FetchSwaps() swapsList, err := d.impl.FetchSwaps()
if err != nil { if err != nil {
// The client is the only thing we started yet, so if we clean // The client and the macaroon service are the only things we
// up its connection now, nothing else needs to be shut down at // started yet, so if we clean that up now, nothing else needs
// this point. // to be shut down at this point.
if err := d.stopMacaroonService(); err != nil {
log.Errorf("Error shutting down macaroon service: %v",
err)
}
clientCleanup() clientCleanup()
return err return err
} }
@ -443,6 +478,11 @@ func (d *Daemon) stop() {
d.restCtxCancel() d.restCtxCancel()
} }
err := d.macaroonService.Close()
if err != nil {
log.Errorf("Error stopping macaroon service: %v", err)
}
// Next, shut down the connections to lnd and the swap server. // Next, shut down the connections to lnd and the swap server.
if d.lnd != nil { if d.lnd != nil {
d.lnd.Close() d.lnd.Close()

@ -0,0 +1,192 @@
package loopd
import (
"context"
"fmt"
"io/ioutil"
"os"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery"
)
const (
// loopMacaroonLocation is the value we use for the loopd macaroons'
// "Location" field when baking them.
loopMacaroonLocation = "loop"
)
var (
// RequiredPermissions is a map of all loop RPC methods and their
// required macaroon permissions to access loopd.
RequiredPermissions = map[string][]bakery.Op{
"/looprpc.SwapClient/LoopOut": {{
Entity: "swap",
Action: "execute",
}, {
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/LoopIn": {{
Entity: "swap",
Action: "execute",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/Monitor": {{
Entity: "swap",
Action: "read",
}},
"/looprpc.SwapClient/ListSwaps": {{
Entity: "swap",
Action: "read",
}},
"/looprpc.SwapClient/SwapInfo": {{
Entity: "swap",
Action: "read",
}},
"/looprpc.SwapClient/LoopOutTerms": {{
Entity: "terms",
Action: "read",
}, {
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/LoopOutQuote": {{
Entity: "swap",
Action: "read",
}, {
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/GetLoopInTerms": {{
Entity: "terms",
Action: "read",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/GetLoopInQuote": {{
Entity: "swap",
Action: "read",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/GetLsatTokens": {{
Entity: "auth",
Action: "read",
}},
}
// allPermissions is the list of all existing permissions that exist
// for loopd's RPC. The default macaroon that is created on startup
// contains all these permissions and is therefore equivalent to lnd's
// admin.macaroon but for loop.
allPermissions = []bakery.Op{{
Entity: "loop",
Action: "out",
}, {
Entity: "loop",
Action: "in",
}, {
Entity: "swap",
Action: "execute",
}, {
Entity: "swap",
Action: "read",
}, {
Entity: "terms",
Action: "read",
}, {
Entity: "auth",
Action: "read",
}}
// macDbDefaultPw is the default encryption password used to encrypt the
// loop macaroon database. The macaroon service requires us to set a
// non-nil password so we set it to an empty string. This will cause the
// keys to be encrypted on disk but won't provide any security at all as
// the password is known to anyone.
//
// TODO(guggero): Allow the password to be specified by the user. Needs
// create/unlock calls in the RPC. Using a password should be optional
// though.
macDbDefaultPw = []byte("")
)
// startMacaroonService starts the macaroon validation service, creates or
// unlocks the macaroon database and creates the default macaroon if it doesn't
// exist yet. If macaroons are disabled in general in the configuration, none of
// these actions are taken.
func (d *Daemon) startMacaroonService() error {
// Create the macaroon authentication/authorization service.
var err error
d.macaroonService, err = macaroons.NewService(
d.cfg.DataDir, loopMacaroonLocation, macaroons.IPLockChecker,
)
if err != nil {
return fmt.Errorf("unable to set up macaroon authentication: "+
"%v", err)
}
// Try to unlock the macaroon store with the private password.
err = d.macaroonService.CreateUnlock(&macDbDefaultPw)
if err != nil {
return fmt.Errorf("unable to unlock macaroon DB: %v", err)
}
// Create macaroon files for loop CLI to use if they don't exist.
if !lnrpc.FileExists(d.cfg.MacaroonPath) {
ctx := context.Background()
// We only generate one default macaroon that contains all
// existing permissions (equivalent to the admin.macaroon in
// lnd). Custom macaroons can be created through the bakery
// RPC.
loopMac, err := d.macaroonService.NewMacaroon(
ctx, macaroons.DefaultRootKeyID,
allPermissions...,
)
if err != nil {
return err
}
loopMacBytes, err := loopMac.M().MarshalBinary()
if err != nil {
return err
}
err = ioutil.WriteFile(d.cfg.MacaroonPath, loopMacBytes, 0644)
if err != nil {
if err := os.Remove(d.cfg.MacaroonPath); err != nil {
log.Errorf("Unable to remove %s: %v",
d.cfg.MacaroonPath, err)
}
return err
}
}
return nil
}
// stopMacaroonService closes the macaroon database.
func (d *Daemon) stopMacaroonService() error {
return d.macaroonService.Close()
}
// macaroonInterceptor creates gRPC server options with the macaroon security
// interceptors.
func (d *Daemon) macaroonInterceptor() []grpc.ServerOption {
unaryInterceptor := d.macaroonService.UnaryServerInterceptor(
RequiredPermissions,
)
streamInterceptor := d.macaroonService.StreamServerInterceptor(
RequiredPermissions,
)
return []grpc.ServerOption{
grpc.UnaryInterceptor(unaryInterceptor),
grpc.StreamInterceptor(streamInterceptor),
}
}

@ -208,7 +208,9 @@ func Run(rpcCfg RPCConfig) error {
// we are running. When our command tries to get a lnd connection, it // we are running. When our command tries to get a lnd connection, it
// blocks until lnd is synced. We listen for interrupts so that we can // blocks until lnd is synced. We listen for interrupts so that we can
// shutdown the daemon while waiting for sync to complete. // shutdown the daemon while waiting for sync to complete.
signal.Intercept() if err := signal.Intercept(); err != nil {
return err
}
// Execute command. // Execute command.
if parser.Active == nil { if parser.Active == nil {

Loading…
Cancel
Save