Skip to content

Commit 053d63e

Browse files
authored
Merge pull request #9546 from hieblmi/macaroon-ip-cidr-constraint
macaroons: ip range constraint
2 parents c4a77d1 + bef0268 commit 053d63e

File tree

5 files changed

+143
-20
lines changed

5 files changed

+143
-20
lines changed

cmd/commands/cmd_macaroon.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ var (
3030
Name: "ip_address",
3131
Usage: "the IP address the macaroon will be bound to",
3232
}
33+
macIPRangeFlag = cli.StringFlag{
34+
Name: "ip_range",
35+
Usage: "the IP range the macaroon will be bound to",
36+
}
3337
macCustomCaveatNameFlag = cli.StringFlag{
3438
Name: "custom_caveat_name",
3539
Usage: "the name of the custom caveat to add",
@@ -557,6 +561,19 @@ func applyMacaroonConstraints(ctx *cli.Context,
557561
)
558562
}
559563

564+
if ctx.IsSet(macIPRangeFlag.Name) {
565+
_, net, err := net.ParseCIDR(ctx.String(macIPRangeFlag.Name))
566+
if err != nil {
567+
return nil, fmt.Errorf("unable to parse ip_range "+
568+
"%s: %w", ctx.String("ip_range"), err)
569+
}
570+
571+
macConstraints = append(
572+
macConstraints,
573+
macaroons.IPLockConstraint(net.String()),
574+
)
575+
}
576+
560577
if ctx.IsSet(macCustomCaveatNameFlag.Name) {
561578
customCaveatName := ctx.String(macCustomCaveatNameFlag.Name)
562579
if containsWhiteSpace(customCaveatName) {

config_builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
472472
}
473473
macaroonService, err = macaroons.NewService(
474474
rootKeyStore, "lnd", walletInitParams.StatelessInit,
475-
macaroons.IPLockChecker,
475+
macaroons.IPLockChecker, macaroons.IPRangeLockChecker,
476476
macaroons.CustomChecker(interceptorChain),
477477
)
478478
if err != nil {

docs/release-notes/release-notes-0.19.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@
166166
on the channel. LND will disable the channel for new HTLCs and kick off the
167167
cooperative close flow automatically when the channel has no HTLCs left.
168168

169+
* [A new macaroon constraint](https://github.com/lightningnetwork/lnd/pull/9546)
170+
to allow for restriction of access based on an IP range. Prior to this only
171+
specific IPs could be allowed or denied.
172+
169173
# Improvements
170174
## Functional Updates
171175

itest/lnd_macaroons_test.go

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
3737
name string
3838
run func(ctxt context.Context, t *testing.T)
3939
}{{
40-
// First test: Make sure we get an error if we use no macaroons
41-
// but try to connect to a node that has macaroon authentication
42-
// enabled.
40+
// Make sure we get an error if we use no macaroons but try to
41+
// connect to a node that has macaroon authentication enabled.
4342
name: "no macaroon",
4443
run: func(ctxt context.Context, t *testing.T) {
4544
conn, err := testNode.ConnectRPCWithMacaroon(nil)
@@ -51,8 +50,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
5150
require.Contains(t, err.Error(), "expected 1 macaroon")
5251
},
5352
}, {
54-
// Second test: Ensure that an invalid macaroon also triggers an
55-
// error.
53+
// Ensure that an invalid macaroon also triggers an error.
5654
name: "invalid macaroon",
5755
run: func(ctxt context.Context, t *testing.T) {
5856
invalidMac, _ := macaroon.New(
@@ -68,8 +66,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
6866
require.Contains(t, err.Error(), "invalid ID")
6967
},
7068
}, {
71-
// Third test: Try to access a write method with read-only
72-
// macaroon.
69+
// Try to access a write method with read-only macaroon.
7370
name: "read only macaroon",
7471
run: func(ctxt context.Context, t *testing.T) {
7572
readonlyMac, err := testNode.ReadMacaroon(
@@ -85,8 +82,8 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
8582
require.Contains(t, err.Error(), "permission denied")
8683
},
8784
}, {
88-
// Fourth test: Check first-party caveat with timeout that
89-
// expired 30 seconds ago.
85+
// Check first-party caveat with timeout that expired 30 seconds
86+
// ago.
9087
name: "expired macaroon",
9188
run: func(ctxt context.Context, t *testing.T) {
9289
readonlyMac, err := testNode.ReadMacaroon(
@@ -106,7 +103,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
106103
require.Contains(t, err.Error(), "macaroon has expired")
107104
},
108105
}, {
109-
// Fifth test: Check first-party caveat with invalid IP address.
106+
// Check first-party caveat with invalid IP address.
110107
name: "invalid IP macaroon",
111108
run: func(ctxt context.Context, t *testing.T) {
112109
readonlyMac, err := testNode.ReadMacaroon(
@@ -128,7 +125,7 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
128125
require.Contains(t, err.Error(), "different IP address")
129126
},
130127
}, {
131-
// Sixth test: Make sure that if we do everything correct and
128+
// Make sure that if we do everything correct and
132129
// send the admin macaroon with first-party caveats that we can
133130
// satisfy, we get a correct answer.
134131
name: "correct macaroon",
@@ -149,8 +146,51 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
149146
assert.Contains(t, res.Address, "bcrt1")
150147
},
151148
}, {
152-
// Seventh test: Bake a macaroon that can only access exactly
153-
// two RPCs and make sure it works as expected.
149+
// Check first-party caveat with invalid IP range.
150+
name: "invalid IP range macaroon",
151+
run: func(ctxt context.Context, t *testing.T) {
152+
readonlyMac, err := testNode.ReadMacaroon(
153+
testNode.Cfg.ReadMacPath, defaultTimeout,
154+
)
155+
require.NoError(t, err)
156+
invalidIPRangeMac, err := macaroons.AddConstraints(
157+
readonlyMac, macaroons.IPRangeLockConstraint(
158+
"1.1.1.1/32",
159+
),
160+
)
161+
require.NoError(t, err)
162+
cleanup, client := macaroonClient(
163+
t, testNode, invalidIPRangeMac,
164+
)
165+
defer cleanup()
166+
_, err = client.GetInfo(ctxt, infoReq)
167+
require.Error(t, err)
168+
require.Contains(t, err.Error(), "different IP range")
169+
},
170+
}, {
171+
// Make sure that if we do everything correct and send the admin
172+
// macaroon with first-party caveats that we can satisfy, we get
173+
// a correct answer.
174+
name: "correct macaroon",
175+
run: func(ctxt context.Context, t *testing.T) {
176+
adminMac, err := testNode.ReadMacaroon(
177+
testNode.Cfg.AdminMacPath, defaultTimeout,
178+
)
179+
require.NoError(t, err)
180+
adminMac, err = macaroons.AddConstraints(
181+
adminMac, macaroons.TimeoutConstraint(30),
182+
macaroons.IPRangeLockConstraint("127.0.0.0/8"),
183+
)
184+
require.NoError(t, err)
185+
cleanup, client := macaroonClient(t, testNode, adminMac)
186+
defer cleanup()
187+
res, err := client.NewAddress(ctxt, newAddrReq)
188+
require.NoError(t, err, "get new address")
189+
assert.Contains(t, res.Address, "bcrt1")
190+
},
191+
}, {
192+
// Bake a macaroon that can only access exactly two RPCs and
193+
// make sure it works as expected.
154194
name: "custom URI permissions",
155195
run: func(ctxt context.Context, t *testing.T) {
156196
entity := macaroons.PermissionEntityCustomURI
@@ -199,9 +239,9 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
199239
require.Contains(t, err.Error(), "permission denied")
200240
},
201241
}, {
202-
// Eighth test: check that with the CheckMacaroonPermissions
203-
// RPC, we can check that a macaroon follows (or doesn't)
204-
// permissions and constraints.
242+
// Check that with the CheckMacaroonPermissions RPC, we can
243+
// check that a macaroon follows (or doesn't) permissions and
244+
// constraints.
205245
name: "unknown permissions",
206246
run: func(ctxt context.Context, t *testing.T) {
207247
// A test macaroon created with permissions from pool,

macaroons/constraints.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package macaroons
33
import (
44
"bytes"
55
"context"
6+
"errors"
67
"fmt"
78
"net"
89
"strings"
@@ -21,6 +22,10 @@ const (
2122
// in the serialized macaroon. We choose a single space as the delimiter
2223
// between the because that is also used by the macaroon bakery library.
2324
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"
2429
)
2530

2631
// CustomCaveatAcceptor is an interface that contains a single method for
@@ -80,9 +85,9 @@ func TimeoutConstraint(seconds int64) func(*macaroon.Macaroon) error {
8085
}
8186
}
8287

83-
// IPLockConstraint locks macaroon to a specific IP address.
84-
// If address is an empty string, this constraint does nothing to
85-
// accommodate default value's desired behavior.
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.
8691
func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error {
8792
return func(mac *macaroon.Macaroon) error {
8893
if ipAddr != "" {
@@ -93,8 +98,32 @@ func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error {
9398
}
9499
caveat := checkers.Condition("ipaddr",
95100
macaroonIPAddr.String())
101+
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+
96124
return mac.AddFirstPartyCaveat([]byte(caveat))
97125
}
126+
98127
return nil
99128
}
100129
}
@@ -122,6 +151,39 @@ func IPLockChecker() (string, checkers.Func) {
122151
}
123152
}
124153

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) {
158+
return CondIPRange, func(ctx context.Context, cond, arg string) error {
159+
// Get peer info and extract IP range from it for macaroon
160+
// check.
161+
pr, ok := peer.FromContext(ctx)
162+
if !ok {
163+
return errors.New("unable to get peer info from " +
164+
"context")
165+
}
166+
peerAddr, _, err := net.SplitHostPort(pr.Addr.String())
167+
if err != nil {
168+
return fmt.Errorf("unable to parse peer address: %w",
169+
err)
170+
}
171+
172+
_, ipNet, err := net.ParseCIDR(arg)
173+
if err != nil {
174+
return fmt.Errorf("unable to parse macaroon IP "+
175+
"range: %w", err)
176+
}
177+
178+
if !ipNet.Contains(net.ParseIP(peerAddr)) {
179+
return errors.New("macaroon locked to different " +
180+
"IP range")
181+
}
182+
183+
return nil
184+
}
185+
}
186+
125187
// CustomConstraint returns a function that adds a custom caveat condition to
126188
// a macaroon.
127189
func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error {

0 commit comments

Comments
 (0)