• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 15561477203

10 Jun 2025 01:54PM UTC coverage: 58.351% (-10.1%) from 68.487%
15561477203

Pull #9356

github

web-flow
Merge 6440b25db into c6d6d4c0b
Pull Request #9356: lnrpc: add incoming/outgoing channel ids filter to forwarding history request

33 of 36 new or added lines in 2 files covered. (91.67%)

28366 existing lines in 455 files now uncovered.

97715 of 167461 relevant lines covered (58.35%)

1.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

51.72
/macaroons/constraints.go
1
package macaroons
2

3
import (
4
        "bytes"
5
        "context"
6
        "errors"
7
        "fmt"
8
        "net"
9
        "strings"
10
        "time"
11

12
        "google.golang.org/grpc/peer"
13
        "gopkg.in/macaroon-bakery.v2/bakery/checkers"
14
        macaroon "gopkg.in/macaroon.v2"
15
)
16

17
const (
18
        // CondLndCustom is the first party caveat condition name that is used
19
        // for all custom caveats in lnd. Every custom caveat entry will be
20
        // encoded as the string
21
        // "lnd-custom <custom-caveat-name> <custom-caveat-condition>"
22
        // in the serialized macaroon. We choose a single space as the delimiter
23
        // between the because that is also used by the macaroon bakery library.
24
        CondLndCustom = "lnd-custom"
25

26
        // CondIPRange is the caveat condition name that is used for tying an IP
27
        // range to a macaroon.
28
        CondIPRange = "iprange"
29
)
30

31
// CustomCaveatAcceptor is an interface that contains a single method for
32
// checking whether a macaroon with the given custom caveat name should be
33
// accepted or not.
34
type CustomCaveatAcceptor interface {
35
        // CustomCaveatSupported returns nil if a macaroon with the given custom
36
        // caveat name can be validated by any component in lnd (for example an
37
        // RPC middleware). If no component is registered to handle the given
38
        // custom caveat then an error must be returned. This method only checks
39
        // the availability of a validating component, not the validity of the
40
        // macaroon itself.
41
        CustomCaveatSupported(customCaveatName string) error
42
}
43

44
// Constraint type adds a layer of indirection over macaroon caveats.
45
type Constraint func(*macaroon.Macaroon) error
46

47
// Checker type adds a layer of indirection over macaroon checkers. A Checker
48
// returns the name of the checker and the checker function; these are used to
49
// register the function with the bakery service's compound checker.
50
type Checker func() (string, checkers.Func)
51

52
// AddConstraints returns new derived macaroon by applying every passed
53
// constraint and tightening its restrictions.
54
func AddConstraints(mac *macaroon.Macaroon,
UNCOV
55
        cs ...Constraint) (*macaroon.Macaroon, error) {
×
UNCOV
56

×
UNCOV
57
        // The macaroon library's Clone() method has a subtle bug that doesn't
×
UNCOV
58
        // correctly clone all caveats. We need to use our own, safe clone
×
UNCOV
59
        // function instead.
×
UNCOV
60
        newMac, err := SafeCopyMacaroon(mac)
×
UNCOV
61
        if err != nil {
×
62
                return nil, err
×
63
        }
×
64

UNCOV
65
        for _, constraint := range cs {
×
UNCOV
66
                if err := constraint(newMac); err != nil {
×
67
                        return nil, err
×
68
                }
×
69
        }
UNCOV
70
        return newMac, nil
×
71
}
72

73
// Each *Constraint function is a functional option, which takes a pointer
74
// to the macaroon and adds another restriction to it. For each *Constraint,
75
// the corresponding *Checker is provided if not provided by default.
76

77
// TimeoutConstraint restricts the lifetime of the macaroon
78
// to the amount of seconds given.
UNCOV
79
func TimeoutConstraint(seconds int64) func(*macaroon.Macaroon) error {
×
UNCOV
80
        return func(mac *macaroon.Macaroon) error {
×
UNCOV
81
                macaroonTimeout := time.Duration(seconds)
×
UNCOV
82
                requestTimeout := time.Now().Add(time.Second * macaroonTimeout)
×
UNCOV
83
                caveat := checkers.TimeBeforeCaveat(requestTimeout)
×
UNCOV
84
                return mac.AddFirstPartyCaveat([]byte(caveat.Condition))
×
UNCOV
85
        }
×
86
}
87

88
// IPLockConstraint locks a macaroon to a specific IP address. If ipAddr is an
89
// empty string, this constraint does nothing to accommodate  default value's
90
// desired behavior.
UNCOV
91
func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error {
×
UNCOV
92
        return func(mac *macaroon.Macaroon) error {
×
UNCOV
93
                if ipAddr != "" {
×
UNCOV
94
                        macaroonIPAddr := net.ParseIP(ipAddr)
×
UNCOV
95
                        if macaroonIPAddr == nil {
×
UNCOV
96
                                return fmt.Errorf("incorrect macaroon IP-" +
×
UNCOV
97
                                        "lock address")
×
UNCOV
98
                        }
×
UNCOV
99
                        caveat := checkers.Condition("ipaddr",
×
UNCOV
100
                                macaroonIPAddr.String())
×
UNCOV
101

×
UNCOV
102
                        return mac.AddFirstPartyCaveat([]byte(caveat))
×
103
                }
104

105
                return nil
×
106
        }
107
}
108

109
// IPRangeLockConstraint locks a macaroon to a specific IP address range. If
110
// ipRange is an empty string, this constraint does nothing to accommodate
111
// default value's desired behavior.
112
func IPRangeLockConstraint(ipRange string) func(*macaroon.Macaroon) error {
×
113
        return func(mac *macaroon.Macaroon) error {
×
114
                if ipRange != "" {
×
115
                        _, parsedNet, err := net.ParseCIDR(ipRange)
×
116
                        if err != nil {
×
117
                                return fmt.Errorf("incorrect macaroon IP "+
×
118
                                        "range: %w", err)
×
119
                        }
×
120
                        caveat := checkers.Condition(
×
121
                                CondIPRange, parsedNet.String(),
×
122
                        )
×
123

×
124
                        return mac.AddFirstPartyCaveat([]byte(caveat))
×
125
                }
126

127
                return nil
×
128
        }
129
}
130

131
// IPLockChecker accepts client IP from the validation context and compares it
132
// with IP locked in the macaroon. It is of the `Checker` type.
133
func IPLockChecker() (string, checkers.Func) {
3✔
134
        return "ipaddr", func(ctx context.Context, cond, arg string) error {
6✔
135
                // Get peer info and extract IP address from it for macaroon
3✔
136
                // check.
3✔
137
                pr, ok := peer.FromContext(ctx)
3✔
138
                if !ok {
3✔
139
                        return fmt.Errorf("unable to get peer info from context")
×
140
                }
×
141
                peerAddr, _, err := net.SplitHostPort(pr.Addr.String())
3✔
142
                if err != nil {
3✔
143
                        return fmt.Errorf("unable to parse peer address")
×
144
                }
×
145

146
                if !net.ParseIP(arg).Equal(net.ParseIP(peerAddr)) {
6✔
147
                        msg := "macaroon locked to different IP address"
3✔
148
                        return fmt.Errorf(msg)
3✔
149
                }
3✔
150
                return nil
3✔
151
        }
152
}
153

154
// IPRangeLockChecker accepts client IP range from the validation context and
155
// compares it with the IP range locked in the macaroon. It is of the `Checker`
156
// type.
157
func IPRangeLockChecker() (string, checkers.Func) {
3✔
158
        return CondIPRange, func(ctx context.Context, cond, arg string) error {
6✔
159
                // Get peer info and extract IP range from it for macaroon
3✔
160
                // check.
3✔
161
                pr, ok := peer.FromContext(ctx)
3✔
162
                if !ok {
3✔
163
                        return errors.New("unable to get peer info from " +
×
164
                                "context")
×
165
                }
×
166
                peerAddr, _, err := net.SplitHostPort(pr.Addr.String())
3✔
167
                if err != nil {
3✔
168
                        return fmt.Errorf("unable to parse peer address: %w",
×
169
                                err)
×
170
                }
×
171

172
                _, ipNet, err := net.ParseCIDR(arg)
3✔
173
                if err != nil {
3✔
174
                        return fmt.Errorf("unable to parse macaroon IP "+
×
175
                                "range: %w", err)
×
176
                }
×
177

178
                if !ipNet.Contains(net.ParseIP(peerAddr)) {
6✔
179
                        return errors.New("macaroon locked to different " +
3✔
180
                                "IP range")
3✔
181
                }
3✔
182

183
                return nil
3✔
184
        }
185
}
186

187
// CustomConstraint returns a function that adds a custom caveat condition to
188
// a macaroon.
UNCOV
189
func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error {
×
UNCOV
190
        return func(mac *macaroon.Macaroon) error {
×
UNCOV
191
                // We rely on a name being set for the interception, so don't
×
UNCOV
192
                // allow creating a caveat without a name in the first place.
×
UNCOV
193
                if name == "" {
×
194
                        return fmt.Errorf("name cannot be empty")
×
195
                }
×
196

197
                // The inner (custom) condition is optional.
UNCOV
198
                outerCondition := fmt.Sprintf("%s %s", name, condition)
×
UNCOV
199
                if condition == "" {
×
UNCOV
200
                        outerCondition = name
×
UNCOV
201
                }
×
202

UNCOV
203
                caveat := checkers.Condition(CondLndCustom, outerCondition)
×
UNCOV
204
                return mac.AddFirstPartyCaveat([]byte(caveat))
×
205
        }
206
}
207

208
// CustomChecker returns a Checker function that is used by the macaroon bakery
209
// library to check whether a custom caveat is supported by lnd in general or
210
// not. Support in this context means: An additional gRPC interceptor was set up
211
// that validates the content (=condition) of the custom caveat. If such an
212
// interceptor is in place then the acceptor should return a nil error. If no
213
// interceptor exists for the custom caveat in the macaroon of a request context
214
// then a non-nil error should be returned and the macaroon is rejected as a
215
// whole.
216
func CustomChecker(acceptor CustomCaveatAcceptor) Checker {
3✔
217
        // We return the general name of all lnd custom macaroons and a function
3✔
218
        // that splits the outer condition to extract the name of the custom
3✔
219
        // condition and the condition itself. In the bakery library that's used
3✔
220
        // here, a caveat always has the following form:
3✔
221
        //
3✔
222
        // <condition-name> <condition-value>
3✔
223
        //
3✔
224
        // Because a checker function needs to be bound to the condition name we
3✔
225
        // have to choose a static name for the first part ("lnd-custom", see
3✔
226
        // CondLndCustom. Otherwise we'd need to register a new Checker function
3✔
227
        // for each custom caveat that's registered. To allow for a generic
3✔
228
        // custom caveat handling, we just add another layer and expand the
3✔
229
        // initial <condition-value> into
3✔
230
        //
3✔
231
        // "<custom-condition-name> <custom-condition-value>"
3✔
232
        //
3✔
233
        // The full caveat string entry of a macaroon that uses this generic
3✔
234
        // mechanism would therefore look like this:
3✔
235
        //
3✔
236
        // "lnd-custom <custom-condition-name> <custom-condition-value>"
3✔
237
        checker := func(_ context.Context, _, outerCondition string) error {
6✔
238
                if outerCondition != strings.TrimSpace(outerCondition) {
3✔
239
                        return fmt.Errorf("unexpected white space found in " +
×
240
                                "caveat condition")
×
241
                }
×
242
                if outerCondition == "" {
3✔
243
                        return fmt.Errorf("expected custom caveat, got empty " +
×
244
                                "string")
×
245
                }
×
246

247
                // The condition part of the original caveat is now name and
248
                // condition of the custom caveat (we add a layer of conditions
249
                // to allow one custom checker to work for all custom lnd
250
                // conditions that implement arbitrary business logic).
251
                parts := strings.Split(outerCondition, " ")
3✔
252
                customCaveatName := parts[0]
3✔
253

3✔
254
                return acceptor.CustomCaveatSupported(customCaveatName)
3✔
255
        }
256

257
        return func() (string, checkers.Func) {
6✔
258
                return CondLndCustom, checker
3✔
259
        }
3✔
260
}
261

262
// HasCustomCaveat tests if the given macaroon has a custom caveat with the
263
// given custom caveat name.
264
func HasCustomCaveat(mac *macaroon.Macaroon, customCaveatName string) bool {
3✔
265
        if mac == nil {
3✔
UNCOV
266
                return false
×
UNCOV
267
        }
×
268

269
        caveatPrefix := []byte(fmt.Sprintf(
3✔
270
                "%s %s", CondLndCustom, customCaveatName,
3✔
271
        ))
3✔
272
        for _, caveat := range mac.Caveats() {
6✔
273
                if bytes.HasPrefix(caveat.Id, caveatPrefix) {
6✔
274
                        return true
3✔
275
                }
3✔
276
        }
277

278
        return false
3✔
279
}
280

281
// GetCustomCaveatCondition returns the custom caveat condition for the given
282
// custom caveat name from the given macaroon.
283
func GetCustomCaveatCondition(mac *macaroon.Macaroon,
284
        customCaveatName string) string {
3✔
285

3✔
286
        if mac == nil {
3✔
287
                return ""
×
288
        }
×
289

290
        caveatPrefix := []byte(fmt.Sprintf(
3✔
291
                "%s %s ", CondLndCustom, customCaveatName,
3✔
292
        ))
3✔
293
        for _, caveat := range mac.Caveats() {
6✔
294
                // The caveat id has a format of
3✔
295
                // "lnd-custom [custom-caveat-name] [custom-caveat-condition]"
3✔
296
                // and we only want the condition part. If we match the prefix
3✔
297
                // part we return the condition that comes after the prefix.
3✔
298
                if bytes.HasPrefix(caveat.Id, caveatPrefix) {
6✔
299
                        caveatSplit := strings.SplitN(
3✔
300
                                string(caveat.Id),
3✔
301
                                string(caveatPrefix),
3✔
302
                                2,
3✔
303
                        )
3✔
304
                        if len(caveatSplit) == 2 {
6✔
305
                                return caveatSplit[1]
3✔
306
                        }
3✔
307
                }
308
        }
309

310
        // We didn't find a condition for the given custom caveat name.
311
        return ""
3✔
312
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc