Skip to content

Commit b3a658d

Browse files
committed
Implement ip allowlist (e.g. geo based)
1 parent 6d7aac1 commit b3a658d

File tree

9 files changed

+215
-31
lines changed

9 files changed

+215
-31
lines changed

library/agent/Agent.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { Context } from "./Context";
1919
import { createTestAgent } from "../helpers/createTestAgent";
2020
import { setTimeout } from "node:timers/promises";
2121

22+
let shouldOnlyAllowSomeIPAddresses = false;
23+
2224
wrap(fetch, "fetch", function mock() {
2325
return async function mock() {
2426
return {
@@ -32,6 +34,15 @@ wrap(fetch, "fetch", function mock() {
3234
},
3335
],
3436
blockedUserAgents: "AI2Bot|Bytespider",
37+
allowedIPAddresses: shouldOnlyAllowSomeIPAddresses
38+
? [
39+
{
40+
source: "name",
41+
description: "Description",
42+
ips: ["4.3.2.1"],
43+
},
44+
]
45+
: [],
3546
}),
3647
};
3748
};
@@ -1099,6 +1110,8 @@ t.test("it does not fetch blocked IPs if serverless", async () => {
10991110
blocked: false,
11001111
});
11011112

1113+
t.same(agent.getConfig().shouldOnlyAllowSomeIPAddresses(), false);
1114+
11021115
t.same(
11031116
agent
11041117
.getConfig()
@@ -1110,3 +1123,28 @@ t.test("it does not fetch blocked IPs if serverless", async () => {
11101123
}
11111124
);
11121125
});
1126+
1127+
t.test("it only allows some IP addresses", async () => {
1128+
shouldOnlyAllowSomeIPAddresses = true;
1129+
const agent = createTestAgent({
1130+
token: new Token("123"),
1131+
suppressConsoleLog: false,
1132+
});
1133+
1134+
agent.start([]);
1135+
1136+
await setTimeout(0);
1137+
1138+
t.same(agent.getConfig().isIPAddressBlocked("1.3.2.4"), {
1139+
blocked: true,
1140+
reason: "Description",
1141+
});
1142+
t.same(agent.getConfig().isIPAddressBlocked("fe80::1234:5678:abcd:ef12"), {
1143+
blocked: true,
1144+
reason: "Description",
1145+
});
1146+
1147+
t.same(agent.getConfig().shouldOnlyAllowSomeIPAddresses(), true);
1148+
t.same(agent.getConfig().isOnlyAllowedIPAddress("1.2.3.4"), false);
1149+
t.same(agent.getConfig().isOnlyAllowedIPAddress("4.3.2.1"), true);
1150+
});

library/agent/Agent.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,15 @@ export class Agent {
4141
private timeoutInMS = 10000;
4242
private hostnames = new Hostnames(200);
4343
private users = new Users(1000);
44-
private serviceConfig = new ServiceConfig([], Date.now(), [], [], true, []);
44+
private serviceConfig = new ServiceConfig(
45+
[],
46+
Date.now(),
47+
[],
48+
[],
49+
true,
50+
[],
51+
[]
52+
);
4553
private routes: Routes = new Routes(200);
4654
private rateLimiter: RateLimiter = new RateLimiter(5000, 120 * 60 * 1000);
4755
private statistics = new InspectionStatistics({
@@ -352,7 +360,7 @@ export class Agent {
352360
this.interval.unref();
353361
}
354362

355-
private async updateBlockedLists() {
363+
async updateBlockedLists() {
356364
if (!this.token) {
357365
return;
358366
}
@@ -363,11 +371,11 @@ export class Agent {
363371
}
364372

365373
try {
366-
const { blockedIPAddresses, blockedUserAgents } = await fetchBlockedLists(
367-
this.token
368-
);
374+
const { blockedIPAddresses, blockedUserAgents, allowedIPAddresses } =
375+
await fetchBlockedLists(this.token);
369376
this.serviceConfig.updateBlockedIPAddresses(blockedIPAddresses);
370377
this.serviceConfig.updateBlockedUserAgents(blockedUserAgents);
378+
this.serviceConfig.updateOnlyAllowedIPAddresses(allowedIPAddresses);
371379
} catch (error: any) {
372380
console.error(`Aikido: Failed to update blocked lists: ${error.message}`);
373381
}

library/agent/ServiceConfig.test.ts

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as t from "tap";
22
import { ServiceConfig } from "./ServiceConfig";
33

44
t.test("it returns false if empty rules", async () => {
5-
const config = new ServiceConfig([], 0, [], [], false, []);
5+
const config = new ServiceConfig([], 0, [], [], false, [], []);
66
t.same(config.getLastUpdatedAt(), 0);
77
t.same(config.isUserBlocked("id"), false);
88
t.same(config.isAllowedIP("1.2.3.4"), false);
@@ -54,6 +54,7 @@ t.test("it works", async () => {
5454
["123"],
5555
[],
5656
false,
57+
[],
5758
[]
5859
);
5960

@@ -81,25 +82,33 @@ t.test("it works", async () => {
8182
});
8283

8384
t.test("it checks if IP is allowed", async () => {
84-
const config = new ServiceConfig([], 0, [], ["1.2.3.4"], false, []);
85+
const config = new ServiceConfig([], 0, [], ["1.2.3.4"], false, [], []);
8586
t.same(config.isAllowedIP("1.2.3.4"), true);
8687
t.same(config.isAllowedIP("1.2.3.5"), false);
8788
});
8889

8990
t.test("ip blocking works", async () => {
90-
const config = new ServiceConfig([], 0, [], [], false, [
91-
{
92-
source: "geoip",
93-
description: "description",
94-
ips: [
95-
"1.2.3.4",
96-
"192.168.2.1/24",
97-
"fd00:1234:5678:9abc::1",
98-
"fd00:3234:5678:9abc::1/64",
99-
"5.6.7.8/32",
100-
],
101-
},
102-
]);
91+
const config = new ServiceConfig(
92+
[],
93+
0,
94+
[],
95+
[],
96+
false,
97+
[
98+
{
99+
source: "geoip",
100+
description: "description",
101+
ips: [
102+
"1.2.3.4",
103+
"192.168.2.1/24",
104+
"fd00:1234:5678:9abc::1",
105+
"fd00:3234:5678:9abc::1/64",
106+
"5.6.7.8/32",
107+
],
108+
},
109+
],
110+
[]
111+
);
103112
t.same(config.isIPAddressBlocked("1.2.3.4"), {
104113
blocked: true,
105114
reason: "description",
@@ -132,14 +141,42 @@ t.test("ip blocking works", async () => {
132141
});
133142

134143
t.test("it blocks bots", async () => {
135-
const config = new ServiceConfig([], 0, [], [], true, []);
144+
const config = new ServiceConfig([], 0, [], [], true, [], []);
136145
config.updateBlockedUserAgents("googlebot|bingbot");
137146

138147
t.same(config.isUserAgentBlocked("googlebot"), { blocked: true });
139148
t.same(config.isUserAgentBlocked("123 bingbot abc"), { blocked: true });
140149
t.same(config.isUserAgentBlocked("bing"), { blocked: false });
141150

151+
t.same(config.shouldOnlyAllowSomeIPAddresses(), false);
152+
142153
config.updateBlockedUserAgents("");
143154

144155
t.same(config.isUserAgentBlocked("googlebot"), { blocked: false });
145156
});
157+
158+
t.test("restricting access to some ips", async () => {
159+
const config = new ServiceConfig(
160+
[],
161+
0,
162+
[],
163+
[],
164+
true,
165+
[],
166+
[
167+
{
168+
source: "geoip",
169+
description: "description",
170+
ips: ["1.2.3.4"],
171+
},
172+
]
173+
);
174+
175+
t.same(config.shouldOnlyAllowSomeIPAddresses(), true);
176+
177+
t.same(config.isOnlyAllowedIPAddress("1.2.3.4"), true);
178+
t.same(config.isOnlyAllowedIPAddress("4.3.2.1"), false);
179+
180+
config.updateOnlyAllowedIPAddresses([]);
181+
t.same(config.isOnlyAllowedIPAddress("1.2.3.4"), false);
182+
});

library/agent/ServiceConfig.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { IPMatcher } from "../helpers/ip-matcher/IPMatcher";
22
import { LimitedContext, matchEndpoints } from "../helpers/matchEndpoints";
33
import { Endpoint } from "./Config";
4-
import { Blocklist as BlocklistType } from "./api/fetchBlockedLists";
4+
import { IPList as IPList } from "./api/fetchBlockedLists";
55

66
export class ServiceConfig {
77
private blockedUserIds: Map<string, string> = new Map();
@@ -11,19 +11,22 @@ export class ServiceConfig {
1111
private blockedIPAddresses: { blocklist: IPMatcher; description: string }[] =
1212
[];
1313
private blockedUserAgentRegex: RegExp | undefined;
14+
private onlyAllowedIPAddresses: IPMatcher | undefined;
1415

1516
constructor(
1617
endpoints: Endpoint[],
1718
private lastUpdatedAt: number,
1819
blockedUserIds: string[],
1920
allowedIPAddresses: string[],
2021
private receivedAnyStats: boolean,
21-
blockedIPAddresses: BlocklistType[]
22+
blockedIPAddresses: IPList[],
23+
onlyAllowedIPAddresses: IPList[]
2224
) {
2325
this.setBlockedUserIds(blockedUserIds);
2426
this.setAllowedIPAddresses(allowedIPAddresses);
2527
this.setEndpoints(endpoints);
2628
this.setBlockedIPAddresses(blockedIPAddresses);
29+
this.setOnlyAllowedIPAddresses(onlyAllowedIPAddresses);
2730
}
2831

2932
private setEndpoints(endpoints: Endpoint[]) {
@@ -96,7 +99,7 @@ export class ServiceConfig {
9699
return { blocked: false };
97100
}
98101

99-
private setBlockedIPAddresses(blockedIPAddresses: BlocklistType[]) {
102+
private setBlockedIPAddresses(blockedIPAddresses: IPList[]) {
100103
this.blockedIPAddresses = [];
101104

102105
for (const source of blockedIPAddresses) {
@@ -107,7 +110,7 @@ export class ServiceConfig {
107110
}
108111
}
109112

110-
updateBlockedIPAddresses(blockedIPAddresses: BlocklistType[]) {
113+
updateBlockedIPAddresses(blockedIPAddresses: IPList[]) {
111114
this.setBlockedIPAddresses(blockedIPAddresses);
112115
}
113116

@@ -126,6 +129,32 @@ export class ServiceConfig {
126129
return { blocked: false };
127130
}
128131

132+
private setOnlyAllowedIPAddresses(ipAddresses: IPList[]) {
133+
this.onlyAllowedIPAddresses = undefined;
134+
135+
if (ipAddresses.length === 0) {
136+
return;
137+
}
138+
139+
const ips = ipAddresses.map((source) => source.ips).flat();
140+
141+
this.onlyAllowedIPAddresses = new IPMatcher(ips);
142+
}
143+
144+
updateOnlyAllowedIPAddresses(ipAddresses: IPList[]) {
145+
this.setOnlyAllowedIPAddresses(ipAddresses);
146+
}
147+
148+
shouldOnlyAllowSomeIPAddresses() {
149+
return this.onlyAllowedIPAddresses !== undefined;
150+
}
151+
152+
isOnlyAllowedIPAddress(ip: string) {
153+
return this.onlyAllowedIPAddresses
154+
? this.onlyAllowedIPAddresses.has(ip)
155+
: false;
156+
}
157+
129158
updateConfig(
130159
endpoints: Endpoint[],
131160
lastUpdatedAt: number,

library/agent/api/fetchBlockedLists.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { fetch } from "../../helpers/fetch";
22
import { getAPIURL } from "../getAPIURL";
33
import { Token } from "./Token";
44

5-
export type Blocklist = {
5+
export type IPList = {
66
source: string;
77
description: string;
88
ips: string[];
99
};
1010

1111
export async function fetchBlockedLists(token: Token): Promise<{
12-
blockedIPAddresses: Blocklist[];
12+
blockedIPAddresses: IPList[];
13+
allowedIPAddresses: IPList[];
1314
blockedUserAgents: string;
1415
}> {
1516
const baseUrl = getAPIURL();
@@ -34,7 +35,8 @@ export async function fetchBlockedLists(token: Token): Promise<{
3435
}
3536

3637
const result: {
37-
blockedIPAddresses: Blocklist[];
38+
blockedIPAddresses: IPList[];
39+
allowedIPAddresses: IPList[];
3840
blockedUserAgents: string;
3941
} = JSON.parse(body);
4042

@@ -43,6 +45,10 @@ export async function fetchBlockedLists(token: Token): Promise<{
4345
result && Array.isArray(result.blockedIPAddresses)
4446
? result.blockedIPAddresses
4547
: [],
48+
allowedIPAddresses:
49+
result && Array.isArray(result.allowedIPAddresses)
50+
? result.allowedIPAddresses
51+
: [],
4652
// Blocked user agents are stored as a string pattern for usage in a regex (e.g. "Googlebot|Bingbot")
4753
blockedUserAgents:
4854
result && typeof result.blockedUserAgents === "string"

library/ratelimiting/getRateLimitedEndpoint.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ const context: Context = {
1818

1919
t.test("it returns undefined if no endpoints", async () => {
2020
t.same(
21-
getRateLimitedEndpoint(context, new ServiceConfig([], 0, [], [], true, [])),
21+
getRateLimitedEndpoint(
22+
context,
23+
new ServiceConfig([], 0, [], [], true, [], [])
24+
),
2225
undefined
2326
);
2427
});
@@ -45,6 +48,7 @@ t.test("it returns undefined if no matching endpoints", async () => {
4548
[],
4649
[],
4750
false,
51+
[],
4852
[]
4953
)
5054
),
@@ -74,6 +78,7 @@ t.test("it returns undefined if matching but not enabled", async () => {
7478
[],
7579
[],
7680
false,
81+
[],
7782
[]
7883
)
7984
),
@@ -103,6 +108,7 @@ t.test("it returns endpoint if matching and enabled", async () => {
103108
[],
104109
[],
105110
false,
111+
[],
106112
[]
107113
)
108114
),
@@ -153,6 +159,7 @@ t.test("it returns endpoint with lowest max requests", async () => {
153159
[],
154160
[],
155161
false,
162+
[],
156163
[]
157164
)
158165
),
@@ -203,6 +210,7 @@ t.test("it returns endpoint with smallest window size", async () => {
203210
[],
204211
[],
205212
false,
213+
[],
206214
[]
207215
)
208216
),
@@ -253,6 +261,7 @@ t.test("it always returns exact matches first", async () => {
253261
[],
254262
[],
255263
false,
264+
[],
256265
[]
257266
)
258267
),

0 commit comments

Comments
 (0)