1use anyhow::{anyhow, bail, Context as _};
2use aya::programs::{Xdp, XdpFlags};
3use clap::Parser;
4#[rustfmt::skip]
5use log::{debug, info, warn};
6use tokio::signal;
7
8use core::net::Ipv6Addr;
9use std::collections::HashSet;
10use std::io::{self, ErrorKind};
11use std::net::{IpAddr, SocketAddr};
12
13use etherip_xdp_common::{iface, ipv6, mac, vlan};
14use ftth_rtnl::{AddressScope, Ipv6Net, RtnlClient};
15
16pub mod interface;
17
18#[derive(Debug, Parser)]
19#[clap(rename_all = "kebab-case")]
20pub struct Opt {
22 #[clap(short = 'o', long, default_value = "eth0")]
24 bind_iface: String,
25
26 #[clap(short = 'i', long, default_value = "eth1")]
28 bridged_iface: String,
29
30 #[clap(
32 long = "tunnel",
33 value_parser = TunnelSpec::parse,
34 default_value = "remote=::1"
35 )]
36 tunnels: Vec<TunnelSpec>,
37}
38
39#[derive(Debug, Clone)]
40enum RemoteAddr {
41 Static(u128),
42 Dynamic(String),
43}
44
45impl RemoteAddr {
46 fn new(addr: &str) -> Result<Self, anyhow::Error> {
47 if let Ok(addr) = addr.parse::<Ipv6Addr>() {
48 Ok(Self::Static(u128::from_be_bytes(addr.octets())))
49 } else {
50 Ok(Self::Dynamic(addr.to_string()))
51 }
52 }
53
54 async fn resolve(&self) -> Result<u128, anyhow::Error> {
55 match self {
56 Self::Static(addr) => Ok(*addr),
57 Self::Dynamic(addr_str) => {
58 let mut addr = tokio::net::lookup_host((addr_str.as_str(), 0)).await?;
59 loop {
60 match addr.next() {
61 Some(SocketAddr::V6(sock_addr)) => {
62 return Ok(u128::from_be_bytes(sock_addr.ip().octets()));
63 }
64
65 None => break,
66 _ => {
67 }
69 }
70 }
71 Err(anyhow::anyhow!("No IPv6 address found for {}", addr_str))
72 }
73 }
74 }
75}
76
77async fn call_blocking_io<F, R>(f: F) -> io::Result<R>
78where
79 F: FnOnce() -> io::Result<R> + Send + 'static,
80 R: Send + 'static,
81{
82 tokio::task::spawn_blocking(f)
83 .await
84 .map_err(|err| io::Error::new(ErrorKind::Other, err))?
85}
86
87#[derive(Debug, Clone)]
88struct TunnelConfig {
89 vlan_id: u16,
90 remote: RemoteAddr,
91}
92
93#[derive(Debug, Clone)]
94struct TunnelSpec {
95 vlan_id: Option<u16>,
96 remote: String,
97}
98
99impl TunnelSpec {
100 fn parse(raw: &str) -> Result<Self, String> {
101 let raw = raw.trim();
102 if raw.is_empty() {
103 return Err("tunnel definition must not be empty".to_string());
104 }
105
106 let mut remote = None;
107 let mut vlan = None;
108
109 for part in raw.split(',') {
110 let part = part.trim();
111 if part.is_empty() {
112 continue;
113 }
114
115 let mut kv = part.splitn(2, '=');
116 let key = kv
117 .next()
118 .map(str::trim)
119 .ok_or_else(|| "malformed tunnel definition".to_string())?;
120 if let Some(value) = kv.next() {
121 let value = value.trim();
122 match key {
123 "remote" => {
124 if remote.is_some() {
125 return Err("duplicate remote entry in tunnel definition".to_string());
126 }
127 if value.is_empty() {
128 return Err("remote address must not be empty".to_string());
129 }
130 remote = Some(value.to_string());
131 }
132 "vlan" => {
133 if vlan.is_some() {
134 return Err("duplicate vlan entry in tunnel definition".to_string());
135 }
136 let parsed = value
137 .parse::<u16>()
138 .map_err(|err| format!("invalid VLAN id '{value}': {err}"))?;
139 vlan = Some(parsed);
140 }
141 _ => {
142 return Err(format!("unknown tunnel option '{key}'"));
143 }
144 }
145 } else {
146 if remote.is_none() {
147 if key.is_empty() {
148 return Err("remote address must not be empty".to_string());
149 }
150 remote = Some(key.to_string());
151 } else {
152 return Err(format!("unexpected tunnel value '{part}'"));
153 }
154 }
155 }
156
157 let remote = remote.ok_or_else(|| "remote address is required for tunnel".to_string())?;
158
159 Ok(Self {
160 vlan_id: vlan,
161 remote,
162 })
163 }
164
165 fn into_config(self) -> anyhow::Result<TunnelConfig> {
166 let vlan_id = self.vlan_id.unwrap_or(vlan::VLAN_ID_NATIVE);
167 if vlan_id != vlan::VLAN_ID_NATIVE && vlan_id > vlan::VLAN_ID_MAX {
168 bail!(
169 "VLAN ID {vlan_id} is out of supported range {}..{}",
170 vlan::VLAN_ID_MIN,
171 vlan::VLAN_ID_MAX
172 );
173 }
174 if vlan_id == vlan::VLAN_ID_LOCAL {
175 bail!("VLAN ID {vlan_id} is reserved for local addressing");
176 }
177
178 let remote = RemoteAddr::new(&self.remote).context("failed to parse remote address")?;
179 Ok(TunnelConfig { vlan_id, remote })
180 }
181}
182
183pub async fn run(opt: Opt) -> anyhow::Result<()> {
184 let Opt {
185 bind_iface,
186 bridged_iface,
187 tunnels,
188 } = opt;
189
190 let rlim = libc::rlimit {
193 rlim_cur: libc::RLIM_INFINITY,
194 rlim_max: libc::RLIM_INFINITY,
195 };
196 let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) };
197 if ret != 0 {
198 debug!("remove limit on locked memory failed, ret is: {}", ret);
199 }
200
201 #[cfg(feature = "build-ebpf")]
206 let mut ebpf = aya::Ebpf::load(aya::include_bytes_aligned!(concat!(
207 env!("OUT_DIR"),
208 "/etherip-xdp"
209 )))?;
210
211 #[cfg(not(feature = "build-ebpf"))]
212 let mut ebpf = aya::Ebpf::load(aya::include_bytes_aligned!(concat!(
213 env!("CARGO_MANIFEST_DIR"),
214 "/assets/etherip-xdp"
215 )))?;
216 if let Err(e) = aya_log::EbpfLogger::init(&mut ebpf) {
217 warn!("failed to initialize eBPF logger: {}", e);
219 }
220
221 let bind_iface_id = interface::name_to_index(&bind_iface)
222 .with_context(|| format!("failed to get interface index for {bind_iface}"))?;
223 let bind_iface_index = bind_iface_id
224 .inner()
225 .ok_or_else(|| anyhow!("interface index for {bind_iface} is unspecified"))?;
226
227 let bridged_iface_id = interface::name_to_index(&bridged_iface)
228 .with_context(|| format!("failed to get interface index for {bridged_iface}"))?;
229 let bridged_iface_index = bridged_iface_id
230 .inner()
231 .ok_or_else(|| anyhow!("interface index for {bridged_iface} is unspecified"))?;
232
233 let mut tunnel_configs = Vec::with_capacity(tunnels.len());
234 let mut seen_vlans = HashSet::with_capacity(tunnels.len());
235
236 for spec in tunnels {
237 let config = spec.into_config()?;
238 if !seen_vlans.insert(config.vlan_id) {
239 bail!("duplicate tunnel configuration for VLAN {}", config.vlan_id);
240 }
241 tunnel_configs.push(config);
242 }
243
244 if !seen_vlans.contains(&vlan::VLAN_ID_NATIVE) {
245 info!("no native VLAN tunnel configured; untagged frames remain on the bridged interface");
246 }
247
248 let tunnels = tunnel_configs;
249
250 let encap_program: &mut Xdp = ebpf
251 .program_mut("etherip_xdp_encap")
252 .ok_or_else(|| anyhow!("eBPF program etherip_xdp_encap not found"))?
253 .try_into()?;
254 encap_program.load()?;
255 encap_program.attach(&bridged_iface, XdpFlags::default())
256 .context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?;
257
258 let decap_program: &mut Xdp = ebpf
259 .program_mut("etherip_xdp_decap")
260 .ok_or_else(|| anyhow!("eBPF program etherip_xdp_decap not found"))?
261 .try_into()?;
262 decap_program.load()?;
263 decap_program.attach(&bind_iface, XdpFlags::default())
264 .context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE")?;
265
266 let mut if_indexes = aya::maps::HashMap::<_, u32, u32>::try_from(
267 ebpf.map_mut("IF_INDEX_MAP")
268 .ok_or_else(|| anyhow!("IF_INDEX_MAP not found"))?,
269 )?;
270
271 if_indexes.insert(iface::IF_INDEX_INNER, bridged_iface_index, 0)?;
272 if_indexes.insert(iface::IF_INDEX_OUTER, bind_iface_index, 0)?;
273
274 info!("XDP program loaded and attached to interfaces");
275
276 let rtnl = RtnlClient::new();
277 let link_client = rtnl.link();
278 let address_client = rtnl.address();
279 let route_client = rtnl.route();
280 let neighbor_client = rtnl.neighbor();
281 let default_ipv6_route_prefix =
282 Ipv6Net::new(Ipv6Addr::UNSPECIFIED, 0).map_err(|err| anyhow!(err))?;
283 let tunnels_task = tunnels.clone();
284 let bind_iface_task = bind_iface.clone();
285 let bind_iface_id_task = bind_iface_id;
286
287 tokio::spawn(async move {
288 let mut ebpf = ebpf;
289 let tunnels = tunnels_task;
290 let bind_iface = bind_iface_task;
291 let address_client = address_client;
292 let route_client = route_client;
293 let neighbor_client = neighbor_client;
294 let link_client = link_client;
295 let default_route_prefix = default_ipv6_route_prefix;
296 let bind_iface_id = bind_iface_id_task;
297
298 loop {
299 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
300
301 let local_if_id = bind_iface_id;
302 let Some(local_if_index) = local_if_id.inner() else {
303 log::error!("local interface index became unspecified");
304 continue;
305 };
306
307 let local_addrs = call_blocking_io({
308 let address_client = address_client.clone();
309 move || {
310 address_client.ipv6_addrs_get_with_scope(
311 Some(local_if_index),
312 Some(AddressScope::Universe),
313 )
314 }
315 })
316 .await?;
317 if local_addrs.is_empty() {
318 log::error!("No global IPv6 address found for interface {}", bind_iface);
319 continue;
320 }
321 let local_addr = local_addrs[0];
322 let local_addr = ipv6::to_u128(local_addr.octets());
323 let mut ipv6_addr_map = aya::maps::HashMap::<_, u16, u128>::try_from(
324 ebpf.map_mut("IPV6_ADDR_MAP")
325 .ok_or_else(|| anyhow!("IPV6_ADDR_MAP not found"))?,
326 )?;
327 ipv6_addr_map.insert(vlan::VLAN_ID_LOCAL, local_addr, 0)?;
328
329 let route = match call_blocking_io({
330 let route_client = route_client.clone();
331 move || route_client.ipv6_route_get_by_prefix(default_route_prefix)
332 })
333 .await
334 {
335 Ok(route) => route,
336 Err(err) if err.kind() == ErrorKind::NotFound => {
337 log::error!("No IPv6 route found");
338 continue;
339 }
340 Err(err) => {
341 log::error!("Failed to retrieve IPv6 default route: {}", err);
342 continue;
343 }
344 };
345
346 let egress_if_index = route
347 .if_id
348 .or_else(|| route.nexthops.iter().find_map(|hop| hop.if_id));
349 let egress_if_index = match egress_if_index {
350 Some(index) if index != 0 => index,
351 _ => {
352 log::error!("No egress interface found for default IPv6 route");
353 continue;
354 }
355 };
356 let gateway_addr = route
357 .gateway
358 .and_then(|gw| match gw {
359 IpAddr::V6(addr) => Some(addr),
360 _ => None,
361 })
362 .or_else(|| {
363 route.nexthops.iter().find_map(|hop| {
364 hop.gateway.and_then(|gw| match gw {
365 IpAddr::V6(addr) => Some(addr),
366 _ => None,
367 })
368 })
369 });
370 let gateway_addr = match gateway_addr {
371 Some(addr) => addr,
372 None => {
373 log::error!("No IPv6 gateway found for default route");
374 continue;
375 }
376 };
377 log::debug!(
378 "Egress interface ID: {}, Gateway address: {}",
379 egress_if_index,
380 gateway_addr
381 );
382
383 let gateway_entry = match call_blocking_io({
384 let neighbor_client = neighbor_client.clone();
385 move || neighbor_client.get(IpAddr::V6(gateway_addr), Some(egress_if_index))
386 })
387 .await
388 {
389 Ok(entry) => entry,
390 Err(err) if err.kind() == ErrorKind::NotFound => {
391 log::error!("No link layer address found for gateway {}", gateway_addr);
392 continue;
393 }
394 Err(err) => {
395 log::error!(
396 "Failed to resolve neighbor entry for gateway {}: {}",
397 gateway_addr,
398 err
399 );
400 continue;
401 }
402 };
403
404 let lladdr_bytes: [u8; 6] = match gateway_entry.link_address {
405 Some(addr) => match addr.as_slice().try_into() {
406 Ok(bytes) => bytes,
407 Err(_) => {
408 log::error!(
409 "Unexpected link layer address length for gateway {}",
410 gateway_addr
411 );
412 continue;
413 }
414 },
415 None => {
416 log::error!("No link layer address found for gateway {}", gateway_addr);
417 continue;
418 }
419 };
420 log::debug!("Gateway's link layer address: {:?}", lladdr_bytes);
421 let lladdr = mac::to_u64(&lladdr_bytes);
422
423 let local_mac = call_blocking_io({
424 let link_client = link_client.clone();
425 move || link_client.mac_addr_get(local_if_index)
426 })
427 .await?;
428 let local_mac = match local_mac {
429 Some(mac) => mac.inner,
430 None => {
431 log::error!("No MAC address found for interface {}", bind_iface);
432 continue;
433 }
434 };
435 log::debug!("Local MAC address: {:?}", local_mac);
436 let local_mac = mac::to_u64(&local_mac);
437 let mut mac_addr_map = aya::maps::HashMap::<_, u32, u64>::try_from(
438 ebpf.map_mut("MAC_ADDR_MAP")
439 .ok_or_else(|| anyhow!("MAC_ADDR_MAP not found"))?,
440 )?;
441 mac_addr_map.insert(mac::MAC_ADDR_LOCAL, local_mac, 0)?;
442 mac_addr_map.insert(mac::MAC_ADDR_GATEWAY, lladdr, 0)?;
443
444 for tunnel in &tunnels {
445 let addr = match tunnel.remote.resolve().await {
446 Ok(addr) => addr,
447 Err(err) => {
448 log::error!(
449 "Failed to resolve remote address for VLAN {}: {}",
450 tunnel.vlan_id,
451 err
452 );
453 continue;
454 }
455 };
456 log::debug!(
457 "Resolved remote address for VLAN {}: {}",
458 tunnel.vlan_id,
459 Ipv6Addr::from(addr)
460 );
461 let mut ipv6_addr_map = aya::maps::HashMap::<_, u16, u128>::try_from(
462 ebpf.map_mut("IPV6_ADDR_MAP")
463 .ok_or_else(|| anyhow!("IPV6_ADDR_MAP not found"))?,
464 )?;
465 ipv6_addr_map.insert(tunnel.vlan_id, addr, 0)?;
466 let mut vlan_id_map = aya::maps::HashMap::<_, u128, u16>::try_from(
467 ebpf.map_mut("VLAN_ID_MAP")
468 .ok_or_else(|| anyhow!("VLAN_ID_MAP not found"))?,
469 )?;
470 vlan_id_map.insert(addr, tunnel.vlan_id, 0)?;
471 }
472
473 tokio::time::sleep(tokio::time::Duration::from_secs(9)).await;
474 }
475
476 #[allow(unreachable_code)]
477 Err::<(), _>(anyhow::Error::msg("Unreachable"))
478 });
479
480 let ctrl_c = signal::ctrl_c();
481 info!("Waiting for Ctrl-C...");
482 ctrl_c.await?;
483 info!("Exiting...");
484
485 Ok(())
486}