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

lightningnetwork / lnd / 13813778123

12 Mar 2025 02:24PM UTC coverage: 68.65% (+0.003%) from 68.647%
13813778123

Pull #9546

github

web-flow
Merge dca2eb21c into 6531d4505
Pull Request #9546: macaroons: ip range constraint

18 of 50 new or added lines in 3 files covered. (36.0%)

68 existing lines in 18 files now uncovered.

130428 of 189991 relevant lines covered (68.65%)

23571.33 hits per line

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

75.86
/macaroons/constraints.go
1
package macaroons
2

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
101
                        return mac.AddFirstPartyCaveat([]byte(caveat))
1✔
102
                }
103

NEW
104
                return nil
×
105
        }
106
}
107

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

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

UNCOV
126
                return nil
×
127
        }
128
}
129

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

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

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

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

177
                if !ipNet.Contains(net.ParseIP(peerAddr)) {
6✔
178
                        msg := "macaroon locked to different IP range"
3✔
179
                        return fmt.Errorf(msg)
3✔
180
                }
3✔
181

182
                return nil
3✔
183
        }
184
}
185

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

196
                // The inner (custom) condition is optional.
197
                outerCondition := fmt.Sprintf("%s %s", name, condition)
2✔
198
                if condition == "" {
3✔
199
                        outerCondition = name
1✔
200
                }
1✔
201

202
                caveat := checkers.Condition(CondLndCustom, outerCondition)
2✔
203
                return mac.AddFirstPartyCaveat([]byte(caveat))
2✔
204
        }
205
}
206

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

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

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

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

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

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

277
        return false
7✔
278
}
279

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

5✔
285
        if mac == nil {
5✔
286
                return ""
×
287
        }
×
288

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

309
        // We didn't find a condition for the given custom caveat name.
310
        return ""
4✔
311
}
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