Skip to content

Commit 042a5ee

Browse files
committed
rules: add RateLimit rule
1 parent 4c8cbe5 commit 042a5ee

File tree

3 files changed

+534
-1
lines changed

3 files changed

+534
-1
lines changed

rules/manager_set.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ type ManagerSet map[string]Manager
1515

1616
// NewRuleManagerSet creates a new map of the supported rule ManagerSet.
1717
func NewRuleManagerSet() ManagerSet {
18-
return map[string]Manager{}
18+
return map[string]Manager{
19+
RateLimitName: &RateLimitMgr{},
20+
}
1921
}
2022

2123
// InitEnforcer gets the appropriate rule Manager for the given name and uses it

rules/rate_limit.go

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package rules
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/lightninglabs/lightning-terminal/firewalldb"
9+
"github.com/lightninglabs/lightning-terminal/litrpc"
10+
"google.golang.org/protobuf/proto"
11+
"gopkg.in/macaroon-bakery.v2/bakery"
12+
)
13+
14+
var (
15+
// Compile-time checks to ensure that RateLimit, RateLimitMgr
16+
// and RateLimitEnforcer implement the appropriate Manager, Enforcer
17+
// and Values interface.
18+
_ Manager = (*RateLimitMgr)(nil)
19+
_ Enforcer = (*RateLimitEnforcer)(nil)
20+
_ Values = (*RateLimit)(nil)
21+
)
22+
23+
// RateLimitName is the string identifier of the RateLimitMgr values.
24+
const RateLimitName = "rate-limit"
25+
26+
// RateLimitMgr represents the rate limit values.
27+
type RateLimitMgr struct{}
28+
29+
// Stop cleans up the resources held by the manager.
30+
//
31+
// NOTE: This is part of the Manager interface.
32+
func (r *RateLimitMgr) Stop() error {
33+
return nil
34+
}
35+
36+
// NewEnforcer constructs a new RateLimit rule enforcer using the passed values
37+
// and config.
38+
//
39+
// NOTE: This is part of the Manager interface.
40+
func (r *RateLimitMgr) NewEnforcer(cfg Config, values Values) (Enforcer,
41+
error) {
42+
43+
limits, ok := values.(*RateLimit)
44+
if !ok {
45+
return nil, fmt.Errorf("values must be of type "+
46+
"RateLimit, got %T", values)
47+
}
48+
49+
return &RateLimitEnforcer{
50+
rateLimitConfig: cfg,
51+
RateLimit: limits,
52+
}, nil
53+
}
54+
55+
// NewValueFromProto converts the given proto value into a RateLimit Value
56+
// object.
57+
//
58+
// NOTE: This is part of the Manager interface.
59+
func (r *RateLimitMgr) NewValueFromProto(v *litrpc.RuleValue) (Values, error) {
60+
rv, ok := v.Value.(*litrpc.RuleValue_RateLimit)
61+
if !ok {
62+
return nil, fmt.Errorf("incorrect RuleValue type")
63+
}
64+
65+
budget := rv.RateLimit
66+
readLim := budget.ReadLimit
67+
writeLim := budget.WriteLimit
68+
69+
return &RateLimit{
70+
ReadLimit: &Rate{
71+
Iterations: readLim.Iterations,
72+
NumHours: readLim.NumHours,
73+
},
74+
WriteLimit: &Rate{
75+
Iterations: writeLim.Iterations,
76+
NumHours: writeLim.NumHours,
77+
},
78+
}, nil
79+
}
80+
81+
// EmptyValue returns a new RateLimit instance.
82+
func (r *RateLimitMgr) EmptyValue() Values {
83+
return &RateLimit{}
84+
}
85+
86+
// rateLimitConfig is the config required by RateLimitMgr. It can be derived
87+
// from the main rules Config struct.
88+
type rateLimitConfig interface {
89+
GetActionsDB() firewalldb.ActionsDB
90+
GetMethodPerms() func(string) ([]bakery.Op, bool)
91+
}
92+
93+
// RateLimitEnforcer enforces requests and responses against a RateLimit rule.
94+
type RateLimitEnforcer struct {
95+
rateLimitConfig
96+
*RateLimit
97+
}
98+
99+
// HandleResponse handles and possible alters a response. This is a noop for the
100+
// RateLimitMgr values.
101+
//
102+
// NOTE: this is part of the Rule interface.
103+
func (r *RateLimitEnforcer) HandleResponse(_ context.Context, _ string,
104+
_ proto.Message) (proto.Message, error) {
105+
106+
return nil, nil
107+
}
108+
109+
// HandleRequest checks the validity of a request. It checks if the request is a
110+
// read or a write request. Then, using the past actions DB, it determines if
111+
// letting this request through would violate the rate limit rules.
112+
//
113+
// NOTE: this is part of the Rule interface.
114+
func (r *RateLimitEnforcer) HandleRequest(ctx context.Context, uri string,
115+
_ proto.Message) (proto.Message, error) {
116+
117+
// First, we need to classify if this is a read or write call.
118+
read := r.isRead(uri)
119+
120+
// Based on the above, we can extract the relevant rate limit values
121+
// that apply for this call.
122+
rateLim := r.WriteLimit
123+
if read {
124+
rateLim = r.ReadLimit
125+
}
126+
127+
// Now we need to go and count all the previous read or write actions.
128+
actions, err := r.GetActionsDB().ListActions(ctx)
129+
if err != nil {
130+
return nil, err
131+
}
132+
133+
// Determine the start time of the actions window.
134+
startTime := time.Now().Add(
135+
-time.Duration(rateLim.NumHours) * time.Hour,
136+
)
137+
138+
// Now count all relevant actions which have taken place after the
139+
// start time.
140+
var count uint32
141+
for _, action := range actions {
142+
if read != r.isRead(action.Method) {
143+
continue
144+
}
145+
146+
if action.PerformedAt.Before(startTime) {
147+
continue
148+
}
149+
150+
count++
151+
}
152+
153+
if count >= rateLim.Iterations {
154+
return nil, fmt.Errorf("too many requests received")
155+
}
156+
157+
return nil, nil
158+
}
159+
160+
// HandleErrorResponse handles and possible alters an error. This is a noop for
161+
// the RateLimitEnforcer rule.
162+
//
163+
// NOTE: this is part of the Enforcer interface.
164+
func (r *RateLimitEnforcer) HandleErrorResponse(_ context.Context, _ string,
165+
_ error) (error, error) {
166+
167+
return nil, nil
168+
}
169+
170+
// isRead is a helper that returns true if the given method/URI only requires
171+
// read-permissions and false otherwise.
172+
func (r *RateLimitEnforcer) isRead(method string) bool {
173+
perms, ok := r.GetMethodPerms()(method)
174+
if !ok {
175+
return false
176+
}
177+
178+
for _, p := range perms {
179+
if p.Action != "read" {
180+
return false
181+
}
182+
}
183+
return true
184+
}
185+
186+
// Rate describes a rate limit in iterations per number of hours.
187+
type Rate struct {
188+
Iterations uint32 `json:"iterations"`
189+
NumHours uint32 `json:"num_hours"`
190+
}
191+
192+
// RateLimit represents the rules values.
193+
type RateLimit struct {
194+
WriteLimit *Rate `json:"write_limit"`
195+
ReadLimit *Rate `json:"read_limit"`
196+
}
197+
198+
// VerifySane checks that the value of the values is ok given the min and max
199+
// allowed values.
200+
//
201+
// NOTE: this is part of the Values interface.
202+
func (r *RateLimit) VerifySane(minVal, maxVal Values) error {
203+
minRL, ok := minVal.(*RateLimit)
204+
if !ok {
205+
return fmt.Errorf("min value is not of type RateLimit")
206+
}
207+
208+
maxRL, ok := maxVal.(*RateLimit)
209+
if !ok {
210+
return fmt.Errorf("max value is not of type RateLimit")
211+
}
212+
213+
// Check that our read limit is between the min and max.
214+
if r.ReadLimit.lessThan(minRL.ReadLimit) ||
215+
maxRL.ReadLimit.lessThan(r.ReadLimit) {
216+
217+
return fmt.Errorf("read limit is not between the min and max")
218+
}
219+
220+
// Check that our write limit is between the min and max.
221+
if r.WriteLimit.lessThan(minRL.WriteLimit) ||
222+
maxRL.WriteLimit.lessThan(r.WriteLimit) {
223+
224+
return fmt.Errorf("write limit is not between the min and max")
225+
}
226+
227+
return nil
228+
}
229+
230+
// lessThan is a helper function that checks if the current rate is less than
231+
// another rate.
232+
func (r *Rate) lessThan(other *Rate) bool {
233+
return float64(r.Iterations)/float64(r.NumHours) <
234+
float64(other.Iterations)/float64(other.NumHours)
235+
}
236+
237+
// RuleName returns the name of the rule that these values are to be used with.
238+
//
239+
// NOTE: this is part of the Values interface.
240+
func (r *RateLimit) RuleName() string {
241+
return RateLimitName
242+
}
243+
244+
// ToProto converts the rule Values to the litrpc counterpart.
245+
//
246+
// NOTE: this is part of the Values interface.
247+
func (r *RateLimit) ToProto() *litrpc.RuleValue {
248+
return &litrpc.RuleValue{
249+
Value: &litrpc.RuleValue_RateLimit{
250+
RateLimit: &litrpc.RateLimit{
251+
ReadLimit: &litrpc.Rate{
252+
Iterations: r.ReadLimit.Iterations,
253+
NumHours: r.ReadLimit.NumHours,
254+
},
255+
WriteLimit: &litrpc.Rate{
256+
Iterations: r.WriteLimit.Iterations,
257+
NumHours: r.WriteLimit.NumHours,
258+
},
259+
},
260+
},
261+
}
262+
}
263+
264+
// PseudoToReal attempts to convert any appropriate pseudo fields in the rule
265+
// Values to their corresponding real values. It uses the passed PrivacyMapDB to
266+
// find the real values. This is a no-op for the RateLimit rule.
267+
//
268+
// NOTE: this is part of the Values interface.
269+
func (r *RateLimit) PseudoToReal(_ firewalldb.PrivacyMapDB) (Values,
270+
error) {
271+
272+
return r, nil
273+
}
274+
275+
// RealToPseudo converts the rule Values to a new one that uses pseudo keys,
276+
// channel IDs, channel points etc. It returns a map of real to pseudo strings
277+
// that should be persisted. This is a no-op for the RateLimit rule.
278+
//
279+
// NOTE: this is part of the Values interface.
280+
func (r *RateLimit) RealToPseudo() (Values, map[string]string, error) {
281+
return r, nil, nil
282+
}

0 commit comments

Comments
 (0)