Skip to main content

etherip_xdp/
lib.rs

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")]
20/// A simple XDP program that encapsulates and decapsulates packets using EtherIP.
21pub struct Opt {
22    /// The outer interface to transmit encapsulated packets.
23    #[clap(short = 'o', long, default_value = "eth0")]
24    bind_iface: String,
25
26    /// The inner interface to be bridged by the EtherIP tunnel.
27    #[clap(short = 'i', long, default_value = "eth1")]
28    bridged_iface: String,
29
30    /// Tunnel definitions mapping VLAN IDs to remote endpoints.
31    #[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                            // Ignore other address types
68                        }
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    // Bump the memlock rlimit. This is needed for older kernels that don't use the
191    // new memcg based accounting, see https://lwn.net/Articles/837122/
192    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    // This will include your eBPF object file as raw bytes at compile-time and load it at
202    // runtime. This approach is recommended for most real-world use cases. If you would
203    // like to specify the eBPF program at runtime rather than at compile-time, you can
204    // reach for `Bpf::load_file` instead.
205    #[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        // This can happen if you remove all log statements from your eBPF program.
218        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}