diff --git a/config_builder.go b/config_builder.go index 43c9e4a68..bb9c1b320 100644 --- a/config_builder.go +++ b/config_builder.go @@ -472,7 +472,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, } macaroonService, err = macaroons.NewService( rootKeyStore, "lnd", walletInitParams.StatelessInit, - macaroons.IPLockChecker, + macaroons.IPLockChecker, macaroons.IPRangeLockChecker, macaroons.CustomChecker(interceptorChain), ) if err != nil { diff --git a/macaroons/constraints.go b/macaroons/constraints.go index 642f8edcf..231fd1a46 100644 --- a/macaroons/constraints.go +++ b/macaroons/constraints.go @@ -3,6 +3,7 @@ package macaroons import ( "bytes" "context" + "errors" "fmt" "net" "strings" @@ -21,6 +22,10 @@ const ( // 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" + + // CondIPRange is the caveat condition name that is used for tying an IP + // range to a macaroon. + CondIPRange = "iprange" ) // CustomCaveatAcceptor is an interface that contains a single method for @@ -80,9 +85,9 @@ func TimeoutConstraint(seconds int64) func(*macaroon.Macaroon) error { } } -// IPLockConstraint locks macaroon to a specific IP address. -// If address is an empty string, this constraint does nothing to -// accommodate default value's desired behavior. +// IPLockConstraint locks a macaroon to a specific IP address. If ipAddr is an +// empty string, this constraint does nothing to accommodate default value's +// desired behavior. func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error { return func(mac *macaroon.Macaroon) error { if ipAddr != "" { @@ -93,8 +98,32 @@ func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error { } caveat := checkers.Condition("ipaddr", macaroonIPAddr.String()) + return mac.AddFirstPartyCaveat([]byte(caveat)) } + + return nil + } +} + +// IPRangeLockConstraint locks a macaroon to a specific IP address range. If +// ipRange is an empty string, this constraint does nothing to accommodate +// default value's desired behavior. +func IPRangeLockConstraint(ipRange string) func(*macaroon.Macaroon) error { + return func(mac *macaroon.Macaroon) error { + if ipRange != "" { + _, parsedNet, err := net.ParseCIDR(ipRange) + if err != nil { + return fmt.Errorf("incorrect macaroon IP "+ + "range: %w", err) + } + caveat := checkers.Condition( + CondIPRange, parsedNet.String(), + ) + + return mac.AddFirstPartyCaveat([]byte(caveat)) + } + return nil } } @@ -122,6 +151,39 @@ func IPLockChecker() (string, checkers.Func) { } } +// IPRangeLockChecker accepts client IP range from the validation context and +// compares it with the IP range locked in the macaroon. It is of the `Checker` +// type. +func IPRangeLockChecker() (string, checkers.Func) { + return CondIPRange, func(ctx context.Context, cond, arg string) error { + // Get peer info and extract IP range from it for macaroon + // check. + pr, ok := peer.FromContext(ctx) + if !ok { + return errors.New("unable to get peer info from " + + "context") + } + peerAddr, _, err := net.SplitHostPort(pr.Addr.String()) + if err != nil { + return fmt.Errorf("unable to parse peer address: %w", + err) + } + + _, ipNet, err := net.ParseCIDR(arg) + if err != nil { + return fmt.Errorf("unable to parse macaroon IP "+ + "range: %w", err) + } + + if !ipNet.Contains(net.ParseIP(peerAddr)) { + return errors.New("macaroon locked to different " + + "IP range") + } + + return nil + } +} + // CustomConstraint returns a function that adds a custom caveat condition to // a macaroon. func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error {