Skip to content

Commit 70146dd

Browse files
committed
serve communities
1 parent e1d9065 commit 70146dd

File tree

2 files changed

+167
-10
lines changed

2 files changed

+167
-10
lines changed

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
preBuild = ''
3030
cp -r ${final.buildPackages.fernglas-frontend} ./static
31+
cp ${final.buildPackages.communities-json} ./src/communities.json
3132
'';
3233

3334
version =

src/api.rs

Lines changed: 166 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::store::{NetQuery, Query, QueryLimits, QueryResult, Store};
22
use axum::body::StreamBody;
3+
use axum::extract::FromRef;
34
use axum::extract::{Query as AxumQuery, State};
45
use axum::http::StatusCode;
56
use axum::response::{IntoResponse, Response};
@@ -10,7 +11,11 @@ use hickory_resolver::config::LookupIpStrategy;
1011
use hickory_resolver::TokioAsyncResolver;
1112
use ipnet::IpNet;
1213
use log::*;
14+
use regex::Regex;
15+
use regex::RegexSet;
1316
use serde::{Deserialize, Serialize};
17+
use std::borrow::Cow;
18+
use std::collections::HashMap;
1419
use std::collections::HashSet;
1520
use std::convert::Infallible;
1621
use std::net::IpAddr;
@@ -21,6 +26,8 @@ use std::sync::Arc;
2126
#[cfg(feature = "embed-static")]
2227
static STATIC_DIR: include_dir::Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/static");
2328

29+
static COMMUNITIES_LIST: &[u8] = include_bytes!("communities.json");
30+
2431
fn default_asn_dns_zone() -> Option<String> {
2532
Some("as{}.asn.cymru.com.".to_string())
2633
}
@@ -36,6 +43,8 @@ pub struct ApiServerConfig {
3643
/// Dns zone used for ASN lookups
3744
#[serde(default = "default_asn_dns_zone")]
3845
pub asn_dns_zone: Option<String>,
46+
/// Path to alternative communities.json
47+
communities_file: Option<String>,
3948
}
4049

4150
#[derive(Debug, Clone, Serialize)]
@@ -49,6 +58,10 @@ pub enum ApiResult {
4958
asn: u32,
5059
asn_name: String,
5160
},
61+
CommunityDescription {
62+
community: String,
63+
community_description: String,
64+
},
5265
}
5366

5467
// Make our own error that wraps `anyhow::Error`.
@@ -76,6 +89,32 @@ where
7689
}
7790
}
7891

92+
#[derive(Clone)]
93+
struct AppState<T: Clone> {
94+
cfg: Arc<ApiServerConfig>,
95+
resolver: TokioAsyncResolver,
96+
community_lists: Arc<CompiledCommunitiesLists>,
97+
store: T,
98+
}
99+
100+
impl<T: Clone> FromRef<AppState<T>> for Arc<ApiServerConfig> {
101+
fn from_ref(app_state: &AppState<T>) -> Self {
102+
app_state.cfg.clone()
103+
}
104+
}
105+
106+
impl<T: Clone> FromRef<AppState<T>> for TokioAsyncResolver {
107+
fn from_ref(app_state: &AppState<T>) -> Self {
108+
app_state.resolver.clone()
109+
}
110+
}
111+
112+
impl<T: Clone> FromRef<AppState<T>> for Arc<CompiledCommunitiesLists> {
113+
fn from_ref(app_state: &AppState<T>) -> Self {
114+
app_state.community_lists.clone()
115+
}
116+
}
117+
79118
async fn parse_or_resolve(resolver: &TokioAsyncResolver, name: String) -> anyhow::Result<IpNet> {
80119
if let Ok(net) = name.parse() {
81120
return Ok(net);
@@ -93,8 +132,82 @@ async fn parse_or_resolve(resolver: &TokioAsyncResolver, name: String) -> anyhow
93132
.into())
94133
}
95134

135+
#[derive(Deserialize)]
136+
struct CommunitiesLists {
137+
regular: CommunitiesList,
138+
large: CommunitiesList,
139+
}
140+
impl CommunitiesLists {
141+
fn compile(self) -> anyhow::Result<CompiledCommunitiesLists> {
142+
Ok(CompiledCommunitiesLists {
143+
regular: self.regular.compile()?,
144+
large: self.large.compile()?,
145+
})
146+
}
147+
}
148+
149+
struct CompiledCommunitiesLists {
150+
regular: CompiledCommunitiesList,
151+
large: CompiledCommunitiesList,
152+
}
153+
154+
#[derive(Deserialize)]
155+
struct CommunitiesList(HashMap<String, String>);
156+
157+
impl CommunitiesList {
158+
fn compile(self) -> anyhow::Result<CompiledCommunitiesList> {
159+
let mut sorted = self.0.into_iter().collect::<Vec<_>>();
160+
sorted.sort_by(|a, b| a.0.len().cmp(&b.0.len()));
161+
Ok(CompiledCommunitiesList {
162+
regex_set: RegexSet::new(sorted.iter().map(|(regex, _desc)| format!("^{}$", regex)))?,
163+
list: sorted
164+
.into_iter()
165+
.map(|(key, value)| Ok((Regex::new(&format!("^{}$", key))?, value)))
166+
.collect::<anyhow::Result<_>>()?,
167+
})
168+
}
169+
}
170+
171+
struct CompiledCommunitiesList {
172+
regex_set: RegexSet,
173+
list: Vec<(Regex, String)>,
174+
}
175+
impl CompiledCommunitiesList {
176+
fn lookup(&self, community: &str) -> Option<Cow<str>> {
177+
self.regex_set
178+
.matches(community)
179+
.iter()
180+
.next()
181+
.map(|index| {
182+
let (regex, desc) = &self.list[index];
183+
let mut desc_templated: Cow<str> = desc.into();
184+
for (i, subcapture) in regex
185+
.captures(community)
186+
.unwrap()
187+
.iter()
188+
.skip(1)
189+
.enumerate()
190+
{
191+
if let Some(subcapture) = subcapture {
192+
let searchstr = format!("${}", i);
193+
if desc_templated.contains(&searchstr) {
194+
desc_templated =
195+
desc_templated.replace(&searchstr, subcapture.into()).into()
196+
}
197+
}
198+
}
199+
desc_templated
200+
})
201+
}
202+
}
203+
96204
async fn query<T: Store>(
97-
State((cfg, resolver, store)): State<(Arc<ApiServerConfig>, TokioAsyncResolver, T)>,
205+
State(AppState {
206+
cfg,
207+
resolver,
208+
store,
209+
community_lists,
210+
}): State<AppState<T>>,
98211
AxumQuery(query): AxumQuery<Query<String>>,
99212
) -> Result<impl IntoResponse, AppError> {
100213
trace!("request: {}", serde_json::to_string_pretty(&query).unwrap());
@@ -126,6 +239,8 @@ async fn query<T: Store>(
126239
// for deduplicating the nexthop resolutions
127240
let mut have_resolved = HashSet::new();
128241
let mut have_asn = HashSet::new();
242+
let mut have_community = HashSet::new();
243+
let mut have_large_community = HashSet::new();
129244

130245
let stream = store
131246
.get_routes(query)
@@ -182,6 +297,35 @@ async fn query<T: Store>(
182297
}
183298
}
184299
}
300+
for community in route.attrs.communities.into_iter().flat_map(|x| x) {
301+
if have_community.insert(community) {
302+
let community_str = format!("{}:{}", community.0, community.1);
303+
if let Some(lookup) = community_lists.regular.lookup(&community_str) {
304+
futures.push(Box::pin(futures_util::future::ready(Some(
305+
ApiResult::CommunityDescription {
306+
community: community_str,
307+
community_description: lookup.to_string(),
308+
},
309+
))));
310+
}
311+
}
312+
}
313+
for large_community in route.attrs.large_communities.into_iter().flat_map(|x| x) {
314+
if have_large_community.insert(large_community) {
315+
let large_community_str = format!(
316+
"{}:{}:{}",
317+
large_community.0, large_community.1, large_community.2
318+
);
319+
if let Some(lookup) = community_lists.large.lookup(&large_community_str) {
320+
futures.push(Box::pin(futures_util::future::ready(Some(
321+
ApiResult::CommunityDescription {
322+
community: large_community_str,
323+
community_description: lookup.to_string(),
324+
},
325+
))));
326+
}
327+
}
328+
}
185329

186330
futures
187331
})
@@ -194,22 +338,35 @@ async fn query<T: Store>(
194338
Ok(StreamBody::new(stream))
195339
}
196340

197-
async fn routers<T: Store>(
198-
State((_, _, store)): State<(Arc<ApiServerConfig>, TokioAsyncResolver, T)>,
199-
) -> impl IntoResponse {
341+
async fn routers<T: Store>(State(AppState { store, .. }): State<AppState<T>>) -> impl IntoResponse {
200342
serde_json::to_string(&store.get_routers()).unwrap()
201343
}
202344

203-
fn make_api<T: Store>(cfg: ApiServerConfig, store: T) -> anyhow::Result<Router> {
345+
async fn make_api<T: Store>(cfg: ApiServerConfig, store: T) -> anyhow::Result<Router> {
204346
let resolver = {
205347
let (rcfg, mut ropts) = hickory_resolver::system_conf::read_system_conf()?;
206348
ropts.ip_strategy = LookupIpStrategy::Ipv6thenIpv4; // strange people set strange default settings
207349
TokioAsyncResolver::tokio(rcfg, ropts)
208350
};
351+
352+
let community_lists: CommunitiesLists = if let Some(ref path) = cfg.communities_file {
353+
let path = path.clone();
354+
serde_json::from_slice(&tokio::task::spawn_blocking(move || std::fs::read(path)).await??)?
355+
} else {
356+
serde_json::from_slice(COMMUNITIES_LIST)?
357+
};
358+
359+
let community_lists = Arc::new(community_lists.compile()?);
360+
209361
Ok(Router::new()
210362
.route("/query", get(query::<T>))
211363
.route("/routers", get(routers::<T>))
212-
.with_state((Arc::new(cfg), resolver, store)))
364+
.with_state(AppState {
365+
cfg: Arc::new(cfg),
366+
resolver,
367+
store,
368+
community_lists,
369+
}))
213370
}
214371

215372
/// This handler serializes the metrics into a string for Prometheus to scrape
@@ -222,8 +379,8 @@ pub async fn get_metrics() -> (StatusCode, String) {
222379

223380
#[cfg(feature = "embed-static")]
224381
async fn static_path(axum::extract::Path(path): axum::extract::Path<String>) -> impl IntoResponse {
225-
use axum::body::Full;
226382
use axum::body::Empty;
383+
use axum::body::Full;
227384
use axum::http::header;
228385
use axum::http::header::HeaderValue;
229386

@@ -259,11 +416,10 @@ pub async fn run_api_server<T: Store>(
259416
}
260417

261418
router = router
262-
.nest("/api", make_api(cfg.clone(), store)?)
419+
.nest("/api", make_api(cfg.clone(), store).await?)
263420
.route("/metrics", get(get_metrics));
264421

265-
let make_service = router
266-
.into_make_service();
422+
let make_service = router.into_make_service();
267423

268424
axum::Server::bind(&cfg.bind)
269425
.serve(make_service)

0 commit comments

Comments
 (0)