diff --git a/network/.gitignore b/network/.gitignore new file mode 100644 index 00000000..6ceaf504 --- /dev/null +++ b/network/.gitignore @@ -0,0 +1,3 @@ +*.log +plugins/**/vendors/ +builds/ diff --git a/network/Documentation/bridge.md b/network/Documentation/bridge.md new file mode 100644 index 00000000..fda90278 --- /dev/null +++ b/network/Documentation/bridge.md @@ -0,0 +1,81 @@ +# bridge plugin + +## Overview + +With bridge plugin, all containers (on the same host) are plugged into a bridge (virtual switch) that resides in the host network namespace. The containers receive one end of the veth pair with the other end connected to the bridge. An IP address is only assigned to one end of the veth pair – one residing in the container. The bridge itself can also be assigned an IP address, turning it into a gateway for the containers. + +The network configuration specifies the name of the bridge to be used. If the bridge is missing, the plugin will create one on first use and, if gateway mode is used, assign it an IP that was returned by IPAM plugin via the gateway field. + +## Summary + +- [bridge plugin](#bridge-plugin) + - [Overview](#overview) + - [Summary](#summary) + - [Section 1: Network configuration reference](#section-1-network-configuration-reference) + - [Required keys](#required-keys) + - [Optional keys](#optional-keys) + - [Example configuration](#example-configuration) + - [Section 2: Interface configuration arguments reference](#section-2-interface-configuration-arguments-reference) + + +## Section 1: Network configuration reference + +This section provides details about the configuration options for the "bridge" CNI plugin. + +### Required keys + +| Implemented | Field | Description | +| ----------- | --------------- | ------------------------ | +| ✅ | `name` (string) | The name of the network. | +| ✅ | `type` (string) | “bridge”. | + +### Optional keys + +| Implemented | Field | Description | +| ----------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ✅ | `bridge` (string) | Name of the bridge to use/create. Defaults to “cni0”. | +| ✅ | `isGateway` (boolean) | Assign an IP address to the bridge. Defaults to false. | +| ✅ | `isDefaultGateway` (boolean) | Sets isGateway to true and makes the assigned IP the default route. Defaults to false. | +| ✅ | `forceAddress` (boolean) | Indicates if a new IP address should be set if the previous value has been changed. Defaults to false. | +| ❌ | `ipMasq` (boolean) | Set up IP Masquerade on the host for traffic originating from this network and destined outside of it. Defaults to false. | +| ❌ | `mtu` (integer) | Explicitly set MTU to the specified value. Defaults to the value chosen by the kernel. | +| ❌ | `hairpinMode` (boolean) | Set hairpin mode for interfaces on the bridge. Defaults to false. | +| ✅ | `ipam` (dictionary) | IPAM configuration to be used for this network. Refer to [host-local](https://github.com/lapsus-ord/orka/blob/cni-impl/network/Documentation/host-local.md) documentation. | +| ✅ | `promiscMode` (boolean) | Set promiscuous mode on the bridge. Defaults to false. | +| ❌ | `vlan` (integer) | Assign VLAN tag. Defaults to none. | +| ❌ | `preserveDefaultVlan` (boolean) | Indicates whether the default vlan must be preserved on the veth end connected to the bridge. Defaults to true. | +| ❌ | `vlanTrunk` (list) | Assign VLAN trunk tag. Defaults to none. | +| ❌ | `enabledad` (boolean) | Enables duplicate address detection for the container side veth. Defaults to false. | +| ❌ | `macspoofchk` (boolean) | Enables mac spoof check, limiting the traffic originating from the container to the mac address of the interface. Defaults to false. | + + +Note: The VLAN parameter configures the VLAN tag on the host end of the veth and also enables the vlan_filtering feature on the bridge interface. + +### Example configuration + +Here's an example configuration for the "bridge" CNI plugin: + +```conf +{ + "cniVersion": "1.0.0", + "name": "orknet", + "type": "bridge", + "bridge": "ork0", + "isDefaultGateway": true, + "ipam": { + "type": "host-local", + "subnet": "10.244.0.0/24", + } +} +``` + +This example demonstrates how to configure the "bridge" plugin, specifying the network name, type, bridge name, default gateway settings, and IPAM configuration. + +## Section 2: Interface configuration arguments reference + +The following `CNI_ARGS` are supported: + + +| Implemented | Field | Description | +| ----------- | -------------- | --------------------------------------------------------------------------------------------- | +| ❌ | `MAC` (string) | Request a specific MAC address for the interface (example: CNI_ARGS=“MAC=c2:11:22:33:44:55”). | diff --git a/network/README.md b/network/README.md new file mode 100644 index 00000000..e1ab0a3a --- /dev/null +++ b/network/README.md @@ -0,0 +1,48 @@ +# Software Defined Network: `orkanet` + +## Overview + +The main plugin used by the runtime (or the CRI) is `orka-cni`, +this plugin will then delegate the creation of interfaces and IPAM +to other plugins like `bridge` and `host-local`. + +The inspiration for this plugin comes from the [CNI plugin](https://github.com/flannel-io/cni-plugin) +of flannel. + +## Summary + +- [Software Defined Network: `orkanet`](#software-defined-network-orkanet) + - [Overview](#overview) + - [Summary](#summary) + - [Section 1: Protocol parameters](#section-1-protocol-parameters) + - [Environment variables](#environment-variables) + - [Errors](#errors) + - [CNI operations](#cni-operations) + - [Section 2: Getting started](#section-2-getting-started) + + +## Section 1: Protocol parameters + +Protocol parameters are passed to the plugins via OS environment variables. + +### Environment variables + +- `CNI_COMMAND`: indicates the desired operation; ADD, DEL, CHECK or VERSION. +- `CNI_CONTAINERID`: Container ID. A unique plaintext identifier for a container, allocated by the runtime. Must not be empty. Must start with an alphanumeric character, optionally followed by any combination of one or more alphanumeric characters, underscore (_), dot (.) or hyphen (-) +- `CNI_NETNS`: A reference to the container's “isolation domain”. If using network namespaces, then a path to the network namespace (e.g., `/run/netns/[nsname]`) +- `CNI_IFNAME`: Name of the interface to create inside the container; if the plugin is unable to use this interface name it must return an error +- `CNI_ARGS`: Extra arguments passed in by the user at invocation time. Alphanumeric key-value pairs separated by semicolons; for example, “FOO=BAR;ABC=123” +- `CNI_PATH`: List of paths to search for CNI plugin executables. Paths are separated by an OS-specific list separator; for example ‘:’ on Linux and ‘;’ on Windows + +### Errors + +A plugin must exit with a return code of 0 on success, and non-zero on failure. If the plugin encounters an error, it should output an “error” result structure (see below). + +### CNI operations + +CNI defines 4 operations: `ADD`, `DEL`, `CHECK`, and `VERSION`. These are passed to the plugin via the `CNI_COMMAND` environment variable. + +## Section 2: Getting started + +To test our CNI plugin, you can use [`cnitool`](https://github.com/containernetworking/cni/tree/main/cnitool), +it is a tool in go to execute CNI configuration. diff --git a/network/build_linux.sh b/network/build_linux.sh new file mode 100755 index 00000000..aeba87df --- /dev/null +++ b/network/build_linux.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Build plugin binaries +export RUSTFLAGS='-A warnings' +mkdir -p builds/ +cd ./plugins/bridge || exit +plugins_names=("bridge" "host-local" "orka-cni") +for str in "${plugins_names[@]}"; do + cd ../"$str" || exit + cargo build --release + cp ./target/release/"$str" ../../builds +done + +# tar them into an archive +cd ../../builds/ || exit +tar czfv ./cni_plugins.tar.gz bridge host-local orka-cni diff --git a/network/plugins/bridge/Cargo.toml b/network/plugins/bridge/Cargo.toml new file mode 100644 index 00000000..f72b078c --- /dev/null +++ b/network/plugins/bridge/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "bridge" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-trait = "0.1.73" +cni-plugin = { version = "0.2", features = ["with-tokio"] } +digest = "0.10.7" +futures = "0.3.28" +libc = "0.2.147" +log = "0.4.20" +netlink-packet-route = "0.17.1" +nix = "0.26.2" +rand = { version = "0.8.5", features = ["small_rng"] } +rtnetlink = "0.13.1" +serde_json = "1.0.105" +sha2 = "0.10.7" +tokio = { version = "1.32.0", features = ["full"] } +which = "4.4.0" diff --git a/network/plugins/bridge/Makefile b/network/plugins/bridge/Makefile new file mode 100644 index 00000000..bac162d7 --- /dev/null +++ b/network/plugins/bridge/Makefile @@ -0,0 +1,32 @@ +PLUGINS_PATH='./target/debug:./vendors' +NET_CONF_PATH='./test' +CNITOOL_PATH='$(HOME)/go/bin/cnitool' +BUILT_BIN_PATH='./target/debug/bridge' + +NETWORK_NAME='orknet' +NS_NAME='testing' + +.cargo-build: + @cargo build + +add: .cargo-build + @sudo CNI_PATH=$(PLUGINS_PATH) NETCONFPATH=$(NET_CONF_PATH) $(CNITOOL_PATH) add $(NETWORK_NAME) /var/run/netns/$(NS_NAME) + @echo '' + +del: .cargo-build + @sudo CNI_PATH=$(PLUGINS_PATH) NETCONFPATH=$(NET_CONF_PATH) $(CNITOOL_PATH) del $(NETWORK_NAME) /var/run/netns/$(NS_NAME) + @echo '' + +check: .cargo-build + @sudo CNI_PATH=$(PLUGINS_PATH) NETCONFPATH=$(NET_CONF_PATH) $(CNITOOL_PATH) check $(NETWORK_NAME) /var/run/netns/$(NS_NAME) + @echo '' + +version: .cargo-build + @echo '{"cniVersion": "1.0.0", "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0" ]}' | sudo CNI_COMMAND=VERSION $(BUILT_BIN_PATH) + @echo '' + +download_vendors: + @mkdir -p ./vendors + @wget -O ./vendors.tgz https://github.com/containernetworking/plugins/releases/download/v1.3.0/cni-plugins-linux-amd64-v1.3.0.tgz + @tar -xvf vendors.tgz -C ./vendors ./host-local + @rm ./vendors.tgz \ No newline at end of file diff --git a/network/plugins/bridge/README.md b/network/plugins/bridge/README.md new file mode 100644 index 00000000..48ed4027 --- /dev/null +++ b/network/plugins/bridge/README.md @@ -0,0 +1,39 @@ +**Note**: This document has moved to [https://github.com/lapsus-ord/orka/blob/cni-impl/network/Documentation/bridge.md](https://github.com/lapsus-ord/orka/blob/cni-impl/network/Documentation/bridge.md). + +## Debug with `cnitool` + +First, install cnitool: + +```bash +go install github.com/containernetworking/cni/cnitool@latest +``` + +Download `host-local` plugin: + +```bash +make download_vendors +``` + +Create a network namespace. This will be called `testing`: + +```bash +sudo ip netns add testing +``` + +**Add** the container to the network: + +```bash +make add +``` + +**Check** whether the container's networking is as expected (ONLY for spec v0.4.0+): + +```bash +make check +``` + +And clean up: + +```bash +make del +``` \ No newline at end of file diff --git a/network/plugins/bridge/src/delegation.rs b/network/plugins/bridge/src/delegation.rs new file mode 100644 index 00000000..a3371ed9 --- /dev/null +++ b/network/plugins/bridge/src/delegation.rs @@ -0,0 +1,170 @@ +use cni_plugin::{config::NetworkConfig, error::CniError, reply::ReplyPayload, Command}; +use log::{debug, error, info}; +use std::io::ErrorKind; +use std::{ + env, + io::Cursor, + path::Path, + process::{ExitStatus, Stdio}, +}; +use tokio::process; +use which::which_in; + +/// Inspired by [cni_plugin::delegation::delegate](https://docs.rs/cni-plugin/latest/cni_plugin/delegation/fn.delegate.html) +/// +/// We removed the `unwrap` and the assertion that can cause `panic`. +pub async fn delegate( + sub_plugin: &str, + command: Command, + config: &NetworkConfig, +) -> Result +where + S: for<'de> ReplyPayload<'de>, +{ + let cwd = env::current_dir().map_err(|_| CniError::NoCwd)?; + let plugin = which_in( + sub_plugin, + Some(env::var("CNI_PATH").map_err(|err| CniError::MissingEnv { + var: "CNI_PATH", + err, + })?), + cwd, + ) + .map_err(|err| CniError::MissingPlugin { + name: sub_plugin.into(), + err, + })?; + + // convert network config into bytes + let config_bytes = serde_json::to_vec(config).map_err(|err| CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err.into()), + })?; + + match delegate_command(&plugin, command, &config_bytes).await { + Ok((status, stdout)) => { + if stdout.is_empty() && !(sub_plugin == "host-local" && command.as_ref() == "DEL") { + if matches!(command, Command::Add) { + delegate_command(&plugin, Command::Del, &config_bytes) + .await + .map_err(|err| CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err), + })?; + } + + return Err(CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(CniError::MissingOutput), + }); + } + + if status.success() { + if sub_plugin == "host-local" && command.as_ref() == "DEL" { + let res: String = format!( + " + {{ + \"cniVersion\": \"{}\", + \"dns\": {{}} + }}", + config.cni_version + ); + Ok( + serde_json::from_str(&res).map_err(|err| CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err.into()), + })?, + ) + } else { + let reader = Cursor::new(stdout); + Ok( + serde_json::from_reader(reader).map_err(|err| CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err.into()), + })?, + ) + } + } else { + if matches!(command, Command::Add) { + delegate_command(&plugin, Command::Del, &config_bytes) + .await + .map_err(|err| CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err), + })?; + } + + Err(CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(CniError::Generic(String::from_utf8_lossy(&stdout).into())), + }) + } + } + Err(err) => { + error!("error running delegate: {}", err); + if matches!(command, Command::Add) { + // We're already failing pretty badly so this is a Just In Case, but + // in all likelihood won't work either. So we ignore any failure. + delegate_command(&plugin, Command::Del, &config_bytes) + .await + .ok(); + } + + Err(CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err), + }) + } + } +} + +async fn delegate_command( + plugin: impl AsRef, + command: impl AsRef, + mut stdin_bytes: &[u8], +) -> Result<(ExitStatus, Vec), CniError> { + let plugin = plugin.as_ref(); + let command = command.as_ref(); + + info!( + "delegating to plugin at {} for command={}", + plugin.display(), + command + ); + + debug!("spawing child process, async=tokio"); + let mut child = process::Command::new(plugin) + .env("CNI_COMMAND", command) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn()?; + + { + debug!("taking child stdin"); + let mut stdin = child.stdin.take().ok_or(std::io::Error::new( + ErrorKind::Other, + "child stdin not found", + ))?; + + debug!("copying bytes={} to stdin", stdin_bytes.len()); + + debug!("before config_len = {:?}", stdin_bytes); + tokio::io::copy_buf(&mut stdin_bytes, &mut stdin).await?; + debug!("after config_len = {:?}", stdin_bytes); + + debug!("dropping stdin handle"); + } + + debug!("awaiting child"); + let output = child.wait_with_output().await?; + + info!( + "delegate plugin at {} for command={} has returned with {}; stdout bytes={}", + plugin.display(), + command, + output.status, + output.stdout.len() + ); + Ok((output.status, output.stdout)) +} diff --git a/network/plugins/bridge/src/ipam.rs b/network/plugins/bridge/src/ipam.rs new file mode 100644 index 00000000..60bee9e6 --- /dev/null +++ b/network/plugins/bridge/src/ipam.rs @@ -0,0 +1,69 @@ +use crate::{ + delegation::delegate, + links::{link::Link, veth::Veth}, + route, +}; +use cni_plugin::{config::NetworkConfig, error::CniError, reply::SuccessReply, Command}; +use std::{collections::HashMap, net::IpAddr}; + +pub async fn exec_cmd(cmd: Command, config: NetworkConfig) -> Result { + delegate::("host-local", cmd, &create_delegation_config(config)?).await +} + +pub fn create_delegation_config(parent_config: NetworkConfig) -> Result { + let NetworkConfig { + cni_version, + name, + args, + prev_result, + runtime, + ipam, + .. + } = parent_config; + + Ok(NetworkConfig { + cni_version, + name, + args, + prev_result, + runtime, + plugin: "host-local".to_string(), + specific: HashMap::new(), + ip_masq: false, + ipam, + dns: None, + }) +} + +pub async fn configure_iface(ifname: String, res: SuccessReply) -> Result<(), CniError> { + let (connection, handle, _) = rtnetlink::new_connection().unwrap(); + tokio::spawn(connection); + + if let Some(ipc) = res.ips.get(0) { + Veth::link_addr_add( + &handle, + ifname.clone(), + ipc.address.ip(), + ipc.address.prefix(), + ) + .await + .map_err(CniError::from)?; + } + + Veth::link_set_up(&handle, ifname.clone()).await?; + + if let Some(ipc) = res.ips.get(0) { + if let Some(IpAddr::V4(gw_addr)) = ipc.gateway { + route::route_add_default(&handle, gw_addr) + .await + .map_err(CniError::from)?; + } else { + return Err(CniError::Generic(format!( + "Failed to convert gateway (from host-local) IpAddr to Ipv4Addr for adding default route to {}", + ifname + ))); + } + } + + Ok(()) +} diff --git a/network/plugins/bridge/src/lib.rs b/network/plugins/bridge/src/lib.rs new file mode 100644 index 00000000..95fe3668 --- /dev/null +++ b/network/plugins/bridge/src/lib.rs @@ -0,0 +1,190 @@ +pub mod delegation; +pub mod ipam; +pub mod links; +pub mod netns; +pub mod route; +pub mod types; + +use crate::types::NetworkConfigReference::*; +use cni_plugin::{ + config::NetworkConfig, + error::CniError, + reply::{Dns, Interface, SuccessReply}, + Command, +}; +use links::{bridge::Bridge, link::Link, veth::Veth}; +use log::info; +use serde_json::json; +use std::{collections::HashMap, net::IpAddr, path::PathBuf}; + +pub async fn cmd_add( + ifname: String, + netns: PathBuf, + mut config: NetworkConfig, +) -> Result { + if let Some(json!(true)) = config.specific.get(&IsDefaultGateway.to_string()) { + config + .specific + .entry(IsGateway.to_string()) + .or_insert_with(|| json!(true)); + } + + let hairpin_mode: bool = config + .specific + .entry(HairpinMode.to_string()) + .or_insert(json!(false)) + .as_bool() + .unwrap(); + let promisc_mode: bool = config + .specific + .entry(PromiscMode.to_string()) + .or_insert(json!(false)) + .as_bool() + .unwrap(); + + if hairpin_mode && promisc_mode { + return Err(CniError::Generic( + "Cannot set hairpin mode and promiscuous mode at the same time. (fn cmd_add)" + .to_string(), + )); + } + + config + .specific + .entry(Mtu.to_string()) + .or_insert(json!(1500)); + config + .specific + .entry(Bridge.to_string()) + .or_insert(json!("cni0")); + config + .specific + .entry(PreserveDefaultVlan.to_string()) + .or_insert(json!(false)); + config + .specific + .entry(ForceAddress.to_string()) + .or_insert(json!(false)); + + // Create bridge only if missing + let (br, br_interface): (Bridge, Interface) = Bridge::setup_bridge(config.clone()).await?; + + // Setup veth pair in container and in host + let (host_interface, container_interface): (Interface, Interface) = Bridge::setup_veth( + br.linkattrs.name.clone(), + netns.clone(), + ifname, + config.clone(), + ) + .await?; + + // Delegate to `host-local` plugin + let ipam_result: SuccessReply = ipam::exec_cmd(Command::Add, config.clone()).await?; + info!("{:?}", ipam_result.ips[0].gateway); + + if ipam_result.ips.is_empty() { + return Err(CniError::Generic( + "IPAM plugin returned missing IP config.".to_string(), + )); + } + + // Last configuration for container interface + netns::exec::<_, _, ()>(netns, |_| async { + ipam::configure_iface(container_interface.name.clone(), ipam_result.clone()).await + }) + .await?; + + let is_gw: bool = config + .specific + .get(&IsGateway.to_string()) + .and_then(|v| v.as_bool()) + .unwrap(); + let force_address: bool = config + .specific + .get(&ForceAddress.to_string()) + .and_then(|v| v.as_bool()) + .unwrap(); + + if is_gw { + for ip in ipam_result.ips.clone() { + let prefix_len: u8 = ip.address.prefix(); + if let Some(gw) = ip.gateway { + let gw_is_ipv4: bool = gw.is_ipv4(); + Bridge::ensure_addr( + br.linkattrs.name.clone(), + gw, + prefix_len, + gw_is_ipv4, + force_address, + ) + .await?; + let _ = route::enable_ip_forward(gw_is_ipv4); + } + } + } + + // Controle oper state is up + Veth::link_check_oper_up(host_interface.name.clone()).await?; + + Ok(SuccessReply { + cni_version: config.cni_version, + interfaces: Vec::from([br_interface, host_interface, container_interface]), + ips: ipam_result.ips, + routes: ipam_result.routes, + dns: ipam_result.dns, + specific: HashMap::new(), + }) +} + +pub async fn cmd_check() -> Result { + todo!(); +} + +pub async fn cmd_del( + ifname: String, + netns: PathBuf, + config: NetworkConfig, +) -> Result { + if netns == PathBuf::from("") { + let _: SuccessReply = ipam::exec_cmd(Command::Del, config.clone()).await?; + } + + // There is a netns so try to clean up. Delete can be called multiple times + // so don't return an error if the device is already removed. + // If the device isn't there then don't try to clean up IP masq either. + let _: IpAddr = match netns::exec::<_, _, IpAddr>(netns, |_| async { + let (connection, handle, _) = rtnetlink::new_connection().unwrap(); + tokio::spawn(connection); + + Veth::del_link_by_name_addr(&handle, ifname).await + }) + .await + { + Ok(addr) => addr, + Err(e) => { + let _: SuccessReply = ipam::exec_cmd(Command::Del, config.clone()).await?; + return Err(e); + } + }; + + // call ipam.ExecDel after clean up device in netns + let _: SuccessReply = ipam::exec_cmd(Command::Del, config.clone()).await?; + + // if ipMasq { + // ipnet + // } + + Ok(SuccessReply { + cni_version: config.cni_version, + interfaces: Vec::from([]), + ips: Vec::new(), + routes: Vec::new(), + dns: Dns { + nameservers: Vec::new(), + domain: None, + search: Vec::new(), + options: Vec::new(), + }, + specific: HashMap::new(), + }) +} diff --git a/network/plugins/bridge/src/links/bridge.rs b/network/plugins/bridge/src/links/bridge.rs new file mode 100644 index 00000000..affed684 --- /dev/null +++ b/network/plugins/bridge/src/links/bridge.rs @@ -0,0 +1,231 @@ +use super::{ + link::{Link, LinkAttrs}, + veth::Veth, +}; +use crate::{netns, types::NetworkConfigReference::*}; +use async_trait::async_trait; +use cni_plugin::{config::NetworkConfig, error::CniError, macaddr::MacAddr, reply::Interface}; +use futures::stream::TryStreamExt; +use log::info; +use rtnetlink::Handle; +use std::{net::IpAddr, path::PathBuf}; + +#[derive(Clone)] +pub struct Bridge { + pub linkattrs: LinkAttrs, + pub promisc_mode: bool, + pub vlan_filtering: bool, +} + +#[async_trait] +impl Link for Bridge { + async fn link_add(&self, handle: &Handle) -> Result<(), CniError> { + let mut links = handle + .link() + .get() + .match_name(self.linkattrs.name.clone()) + .execute(); + match links.try_next().await { + Ok(Some(_)) => Ok(()), + _ => handle + .link() + .add() + .bridge(self.linkattrs.name.clone()) + .execute() + .await + .map_err(|err| { + CniError::Generic(format!( + "Could not add link {} type bridge. (fn link_add)\n{}\n", + self.linkattrs.name, err + )) + }), + } + } +} + +impl Bridge { + pub async fn setup_bridge(config: NetworkConfig) -> Result<(Self, Interface), CniError> { + let (connection, handle, _) = rtnetlink::new_connection().unwrap(); + tokio::spawn(connection); + + let vlan_filtering: bool = config + .specific + .get(&Vlan.to_string()) + .and_then(|value| value.as_i64()) + .map(|i| i == 0 || config.specific.contains_key("vlanTrunk")) + .unwrap_or(false); + let promisc_mode: bool = config + .specific + .get(&PromiscMode.to_string()) + .and_then(|value| value.as_bool()) + .unwrap_or(false); + let br_name: String = config + .specific + .get(&Bridge.to_string()) + .and_then(|value| value.as_str()) + .unwrap() + .to_string(); + let mtu: i64 = config + .specific + .get(&Mtu.to_string()) + .and_then(|value| value.as_i64()) + .unwrap(); + + let br: Bridge = + Bridge::ensure_bridge(&handle, br_name, mtu, promisc_mode, vlan_filtering).await?; + + Ok(( + br.clone(), + Interface { + name: br.linkattrs.name, + mac: br.linkattrs.hardware_addr, + sandbox: String::new().into(), + }, + )) + } + + async fn ensure_bridge( + handle: &Handle, + br_name: String, + mtu: i64, + promisc_mode: bool, + vlan_filtering: bool, + ) -> Result { + let br: Bridge = Self { + linkattrs: LinkAttrs { + name: br_name, + mtu, + txqlen: -1, + hardware_addr: Option::None, + }, + promisc_mode, + vlan_filtering, + }; + + br.link_add(handle).await?; + + if br.promisc_mode { + Bridge::link_promisc_on(handle, br.linkattrs.name.clone()).await?; + } + + // Re-fetch link to read all attributes and if it already existed, + // ensure it's really a bridge with similar configuration + // Self::bridge_by_name(handle, br.linkattrs.name.clone()).await; + + Bridge::link_set_up(handle, br.linkattrs.name.clone()).await?; + + Ok(br) + } + + pub async fn setup_veth( + br_name: String, + netns: PathBuf, + ifname: String, + config: NetworkConfig, + ) -> Result<(Interface, Interface), CniError> { + let mtu: i64 = config + .specific + .get(&Mtu.to_string()) + .and_then(|value| value.as_i64()) + .unwrap(); + + // config.args.get("MAC"); + let mac: Option = Option::None; + + // Handle for host namespace + let (connection_host, handle_host, _) = rtnetlink::new_connection().unwrap(); + tokio::spawn(connection_host); + + let handle_host_for_cont: Handle = handle_host.clone(); + let (host_veth_name, cont_veth) = + netns::exec::<_, _, (String, Veth)>(netns.clone(), |host_ns_fd| async move { + // Handle for container namespace + let (connection_cont, handle_cont, _) = rtnetlink::new_connection().unwrap(); + tokio::spawn(connection_cont); + + // create the veth pair in the container + Veth::setup_veth( + &handle_host_for_cont, + &handle_cont, + ifname, + mtu, + mac, + host_ns_fd, + ) + .await + }) + .await?; + + // connect host veth end to the bridge + Self::link_set_master(&handle_host, host_veth_name.clone(), br_name).await?; + + // ? set hairpin mode ? + // ? remove default vlan ? + // ? Currently bridge CNI only support access port(untagged only) or trunk port(tagged only) ? + + let cont_iface = Interface { + name: cont_veth.linkattrs.name, + mac: Option::None, + sandbox: netns, + }; + let host_iface = Interface { + name: host_veth_name, + mac: Option::None, + sandbox: PathBuf::from(format!("/proc/self/fd/{}", cont_veth.peer_namespace)), + }; + + Ok((host_iface, cont_iface)) + } + + pub async fn ensure_addr( + name: String, + gw_addr: IpAddr, + prefix_len: u8, + gw_is_ipv4: bool, + force_address: bool, + ) -> Result<(), CniError> { + let (connection, handle, _) = rtnetlink::new_connection().unwrap(); + tokio::spawn(connection); + + if let Some(current_addr) = + Self::link_get_addr(&handle, name.clone()) + .await + .map_err(|_| { + CniError::Generic(format!( + "Failed to get current IP address for {}. (fn ensure_addr)", + name + )) + })? + { + info!("CURRENT IP: {}", current_addr); + + if current_addr == gw_addr { + return Ok(()); + } + + // Multiple IPv6 addresses are allowed on the bridge if the + // corresponding subnets do not overlap. For IPv4 or for + // overlapping IPv6 subnets, reconfigure the IP address if + // forceAddress is true, otherwise throw an error. + if current_addr.is_ipv4() { + if force_address { + Bridge::link_delete_addr(&handle, name.clone()).await?; + } else { + return Err(CniError::Generic(format!( + "{} already has an IP address different from {:?}", + name, gw_addr + ))); + } + } + } + + if gw_is_ipv4 { + Bridge::link_addr_add(&handle, name, gw_addr, prefix_len).await + } else { + Err(CniError::Generic(format!( + "Gateway address is not ipv4 : {}. (fn ensure_addr)", + gw_addr + ))) + } + } +} diff --git a/network/plugins/bridge/src/links/link.rs b/network/plugins/bridge/src/links/link.rs new file mode 100644 index 00000000..d114d389 --- /dev/null +++ b/network/plugins/bridge/src/links/link.rs @@ -0,0 +1,282 @@ +use async_trait::async_trait; +use cni_plugin::{error::CniError, macaddr::MacAddr}; +use futures::stream::TryStreamExt; +use netlink_packet_route::{ + address, + nlas::link::{self, State}, + LinkMessage, +}; +use rtnetlink::Handle; +use std::{net::IpAddr, thread::sleep, time::Duration}; + +use crate::links::utils::convert_to_ip; + +#[derive(Clone)] +pub struct LinkAttrs { + pub name: String, + pub mtu: i64, + // Let kernel use default txqueuelen; leaving it unset + // means 0, and a zero-length TX queue messes up FIFO + // traffic shapers which use TX queue length as the + // default packet limit + // #[derivative(Default(value = "-1"))] + pub txqlen: i8, + pub hardware_addr: Option, +} + +#[async_trait] +pub trait Link { + async fn link_add(&self, handle: &Handle) -> Result<(), CniError>; + + async fn link_by_name(handle: &Handle, name: String) -> Result { + let mut links = handle.link().get().match_name(name.clone()).execute(); + match links.try_next().await { + Ok(Some(link)) => Ok(link), + _ => { + return Err(CniError::Generic(format!( + "Failed to get link {}. (fn link_addr_add)", + name + ))) + } + } + } + + async fn link_addr_add( + handle: &Handle, + name: String, + addr: IpAddr, + prefix_len: u8, + ) -> Result<(), CniError> { + let mut links = handle.link().get().match_name(name.clone()).execute(); + match links.try_next().await { + Ok(Some(link)) => handle + .address() + .add(link.header.index, addr, prefix_len) + .execute() + .await + .map_err(|e| { + CniError::Generic(format!( + "Failed to add address {}/{} to {}. (fn link_addr_add) {}", + addr, prefix_len, name, e + )) + }), + _ => { + return Err(CniError::Generic(format!( + "Failed to add address {}/{} to {}. (fn link_addr_add)", + addr, prefix_len, name + ))) + } + } + } + + async fn link_set_master( + handle: &Handle, + veth_peer_name: String, + br_name: String, + ) -> Result<(), CniError> { + let mut links = handle.link().get().match_name(br_name.clone()).execute(); + let master_index = match links.try_next().await { + Ok(Some(link)) => link.header.index, + _ => { + return Err(CniError::Generic(format!( + "Cannot get link {} type bridge. (fn link_set_master)", + br_name + ))) + } + }; + + let mut links = handle + .link() + .get() + .match_name(veth_peer_name.clone()) + .execute(); + match links.try_next().await { + Ok(Some(link)) => handle + .link() + .set(link.header.index) + .master(master_index) + .execute() + .await + .map_err(|e| { + CniError::Generic(format!( + "Failed to connect {} to bridge {}. (fn link_set_master) {}", + veth_peer_name, br_name, e + )) + }), + _ => Err(CniError::Generic(format!( + "Failed to connect {} to bridge {}. (fn link_set_master)", + veth_peer_name, br_name + ))), + } + } + + async fn link_promisc_on(handle: &Handle, name: String) -> Result<(), CniError> { + let mut links = handle.link().get().match_name(name.clone()).execute(); + match links.try_next().await { + Ok(Some(link)) => handle + .link() + .set(link.header.index) + .promiscuous(true) + .execute() + .await + .map_err(|e| { + CniError::Generic(format!( + "Could not set promiscuous mode on for {}. (fn link_promisc_on) {}", + name, e + )) + }), + _ => Err(CniError::Generic(format!( + "Could not set promiscuous mode on for {}. (fn link_promisc_on)", + name + ))), + } + } + + async fn link_set_up(handle: &Handle, name: String) -> Result<(), CniError> { + let mut links = handle.link().get().match_name(name.clone()).execute(); + match links.try_next().await { + Ok(Some(link)) => handle + .link() + .set(link.header.index) + .up() + .execute() + .await + .map_err(|e| { + CniError::Generic(format!("Could not set up {}. (fn link_set_up) {}", name, e)) + }), + _ => Err(CniError::Generic(format!( + "Could not set up {}. (fn link_set_up)", + name + ))), + } + } + + async fn link_check_oper_up(name: String) -> Result<(), CniError> { + let (connection, handle, _) = rtnetlink::new_connection().unwrap(); + tokio::spawn(connection); + + let retries: Vec = vec![0, 50, 500, 1000, 1000]; + for (idx, &sleep_duration) in retries.iter().enumerate() { + sleep(Duration::from_millis(sleep_duration as u64)); + + let host_veth: LinkMessage = Self::link_by_name(&handle, name.clone()).await?; + let option_index: Option = host_veth + .nlas + .iter() + .position(|nla| nla == &link::Nla::OperState(State::Up)); + + if let Some(_) = option_index { + break; + } else { + if idx == 4 { + return Err(CniError::Generic(format!( + "Interface {} cannot oper up. (fn link_check_oper_up)", + name + ))); + } + } + } + + Ok(()) + } + + async fn del_link_by_name_addr(handle: &Handle, name: String) -> Result { + let addr: IpAddr; + if let Some(ip) = Self::link_get_addr(handle, name.clone()).await? { + addr = ip; + } else { + return Err(CniError::Generic(format!( + "Failed to delete address for del link {}. (fn del_link_by_name_addr)", + name + ))); + } + let mut links = handle.link().get().match_name(name.clone()).execute(); + match links.try_next().await { + Ok(Some(link)) => handle + .link() + .del(link.header.index) + .execute() + .await + .map_err(|e| { + CniError::Generic(format!( + "Failed to delete link {}. (fn del_link_by_name_addr) {}", + name, e + )) + }), + _ => Err(CniError::Generic(format!( + "Failed to delete link {}. (fn del_link_by_name_addr)", + name + ))), + }?; + + Ok(addr) + } + + async fn link_get_addr(handle: &Handle, name: String) -> Result, CniError> { + let mut links = handle.link().get().match_name(name.clone()).execute(); + match links.try_next().await { + Ok(Some(link)) => { + let mut addresses = handle + .address() + .get() + .set_link_index_filter(link.header.index) + .execute(); + match addresses.try_next().await { + Ok(Some(addr)) => { + let addresses: Vec = addr + .nlas + .iter() + .filter_map(|nla| { + if let address::Nla::Address(addr) = nla.to_owned() { + Some(addr) + } else { + None + } + }) + .flatten() + .collect(); + + Ok(convert_to_ip(addresses)) + } + _ => Err(CniError::Generic(format!( + "Failed to get IP addresses for {}. (fn link_get_addr)", + name + ))), + } + } + _ => Err(CniError::Generic(format!( + "Failed to get IP addresses for {}. (fn link_get_addr)", + name + ))), + } + } + + async fn link_delete_addr(handle: &Handle, name: String) -> Result<(), CniError> { + let mut links = handle.link().get().match_name(name.clone()).execute(); + match links.try_next().await { + Ok(Some(link)) => { + let mut addresses = handle + .address() + .get() + .set_link_index_filter(link.header.index) + .execute(); + match addresses.try_next().await { + Ok(Some(addr)) => handle.address().del(addr).execute().await.map_err(|e| { + CniError::Generic(format!( + "Could not remove IP address from {}. (fn link_delete_addr) {}", + name, e + )) + }), + _ => Err(CniError::Generic(format!( + "Could not remove IP address from {}. (fn link_delete_addr)", + name + ))), + } + } + _ => Err(CniError::Generic(format!( + "Could not remove IP address from {}. (fn link_delete_addr)", + name + ))), + } + } +} diff --git a/network/plugins/bridge/src/links/mod.rs b/network/plugins/bridge/src/links/mod.rs new file mode 100644 index 00000000..4aae7b53 --- /dev/null +++ b/network/plugins/bridge/src/links/mod.rs @@ -0,0 +1,4 @@ +pub mod bridge; +pub mod link; +pub mod utils; +pub mod veth; diff --git a/network/plugins/bridge/src/links/utils.rs b/network/plugins/bridge/src/links/utils.rs new file mode 100644 index 00000000..f1e2f3d1 --- /dev/null +++ b/network/plugins/bridge/src/links/utils.rs @@ -0,0 +1,32 @@ +use digest::Digest; +use rand::distributions::Alphanumeric; +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; +use sha2::Sha256; +use std::iter::Iterator; +use std::net::{IpAddr, Ipv4Addr}; + +// RandomVethName returns string "veth" with random prefix (hashed from entropy) +pub fn random_veth_name() -> String { + let rng: SmallRng = SmallRng::from_entropy(); + let random_prefix: String = rng + .sample_iter(&Alphanumeric) + .take(8) + .map(char::from) + .collect(); + + let mut hasher = Sha256::new(); + hasher.update(random_prefix.as_bytes()); + let hash_result = hasher.finalize(); + let hash_prefix = format!("{:x}", hash_result); + + format!("veth-{}", &hash_prefix[..8]) +} + +pub fn convert_to_ip(vec: Vec) -> Option { + if vec.len() == 4 { + Some(IpAddr::V4(Ipv4Addr::new(vec[0], vec[1], vec[2], vec[3]))) + } else { + None + } +} diff --git a/network/plugins/bridge/src/links/veth.rs b/network/plugins/bridge/src/links/veth.rs new file mode 100644 index 00000000..0cd54722 --- /dev/null +++ b/network/plugins/bridge/src/links/veth.rs @@ -0,0 +1,174 @@ +use super::link::{Link, LinkAttrs}; +use super::utils; +use async_trait::async_trait; +use cni_plugin::error::CniError; +use cni_plugin::macaddr::MacAddr; +use futures::stream::TryStreamExt; +use rtnetlink::Handle; + +#[derive(Clone)] +pub struct Veth { + pub linkattrs: LinkAttrs, + pub peer_name: String, + pub peer_namespace: i32, +} + +#[async_trait] +impl Link for Veth { + async fn link_add(&self, handle: &Handle) -> Result<(), CniError> { + let mut links = handle + .link() + .get() + .match_name(self.linkattrs.name.clone()) + .execute(); + match links.try_next().await { + Ok(Some(_)) => Err(CniError::Generic(format!( + "Container veth name provided `{}` already exists. (fn link_add)", + self.linkattrs.name + ))), + _ => handle + .link() + .add() + .veth(self.linkattrs.name.clone(), self.peer_name.clone()) + .execute() + .await + .map_err(|e| { + CniError::Generic(format!( + "Failed to add veth pair: {} and {} (peer). (fn link_add) {}", + self.linkattrs.name, self.peer_name, e + )) + }), + }?; + + let mut links = handle + .link() + .get() + .match_name(self.peer_name.clone()) + .execute(); + match links.try_next().await { + Ok(Some(link)) => handle + .link() + .set(link.header.index) + .setns_by_fd(self.peer_namespace) + .execute() + .await + .map_err(|e| { + CniError::Generic(format!( + "Failed to set {} (veth peer) in host namespace. (fn link_add) {}", + self.peer_name, e + )) + }), + _ => Err(CniError::Generic(format!( + "Failed to set {} (veth peer) in host namespace. (fn link_add)", + self.peer_name + ))), + } + } +} + +impl Veth { + // SetupVeth sets up a pair of virtual ethernet devices. + // Call SetupVeth from inside the container netns. It will create both veth + // devices and move the host-side veth into the provided hostNS namespace. + // On success, SetupVeth returns (hostVeth, containerVeth, nil) + pub async fn setup_veth( + handle_host: &Handle, + handle_cont: &Handle, + cont_veth_name: String, + mtu: i64, + cont_veth_mac: Option, + host_ns_fd: i32, + ) -> Result<(String, Self), CniError> { + Self::setup_veth_with_name( + handle_host, + handle_cont, + cont_veth_name, + String::new(), + mtu, + cont_veth_mac, + host_ns_fd, + ) + .await + } + + async fn setup_veth_with_name( + handle_host: &Handle, + handle_cont: &Handle, + cont_veth_name: String, + host_veth_name: String, + mtu: i64, + cont_veth_mac: Option, + host_ns_fd: i32, + ) -> Result<(String, Self), CniError> { + let (host_veth_name, cont_veth) = Self::make_veth( + handle_cont, + cont_veth_name, + host_veth_name, + mtu, + cont_veth_mac, + host_ns_fd, + ) + .await?; + + Veth::link_set_up(handle_host, host_veth_name.clone()).await?; + + Ok((host_veth_name, cont_veth)) + } + + pub async fn make_veth( + handle: &Handle, + name: String, + veth_peer_name: String, + mtu: i64, + mac: Option, + host_ns_fd: i32, + ) -> Result<(String, Self), CniError> { + let peer_name: String = if veth_peer_name.is_empty() { + utils::random_veth_name() + } else { + veth_peer_name + }; + let veth: Veth = Self::make_veth_pair( + handle, + name.clone(), + peer_name.clone(), + mtu, + mac, + host_ns_fd, + ) + .await?; + + Ok((peer_name, veth)) + } + + async fn make_veth_pair( + handle: &Handle, + name: String, + peer: String, + mtu: i64, + mac: Option, + host_ns_fd: i32, + ) -> Result { + let mut veth: Self = Veth { + linkattrs: LinkAttrs { + name, + mtu, + txqlen: -1, + hardware_addr: Option::None, + }, + peer_name: peer, + peer_namespace: host_ns_fd, + }; + + // MAC addr is set but not set... + if let Some(addr) = mac { + veth.linkattrs.hardware_addr = Some(addr); + } + + veth.link_add(handle).await?; + + // ? Re-fetch the container link to get its creation-time parameters, e.g. index and mac ? + + Ok(veth) + } +} diff --git a/network/plugins/bridge/src/main.rs b/network/plugins/bridge/src/main.rs new file mode 100644 index 00000000..3ce75e92 --- /dev/null +++ b/network/plugins/bridge/src/main.rs @@ -0,0 +1,52 @@ +use cni_plugin::config::NetworkConfig; +use cni_plugin::reply::{reply, SuccessReply}; +use cni_plugin::{error::CniError, logger, Cni}; +use tokio::runtime::Runtime; + +fn main() { + logger::install("bridge.log"); + + if let Ok(runtime) = Runtime::new() { + match Cni::load() { + Cni::Add { + ifname, + netns, + config, + .. + } => { + runtime.block_on(async move { + let result: Result = + bridge::cmd_add(ifname, netns, config.clone()).await; + into_reply(result, &config).await; + }); + } + Cni::Del { + ifname, + netns, + config, + .. + } => { + runtime.block_on(async move { + let result: Result = + bridge::cmd_del(ifname, netns.unwrap(), config.clone()).await; + into_reply(result, &config).await; + }); + } + Cni::Check { config, .. } => { + runtime.block_on(async move { + let result: Result = bridge::cmd_check().await; + into_reply(result, &config).await; + }); + } + Cni::Version(_) => unreachable!(), + } + } +} + +async fn into_reply(result: Result, config: &NetworkConfig) { + let NetworkConfig { cni_version, .. } = config; + match result { + Ok(success) => reply(success), + Err(cni_error) => reply(cni_error.into_reply(cni_version.clone())), + } +} diff --git a/network/plugins/bridge/src/netns.rs b/network/plugins/bridge/src/netns.rs new file mode 100644 index 00000000..35849408 --- /dev/null +++ b/network/plugins/bridge/src/netns.rs @@ -0,0 +1,60 @@ +use cni_plugin::error::CniError; +use futures::Future; +use nix::{ + fcntl::{open, OFlag}, + sched::CloneFlags, + sys::stat::Mode, +}; +use std::{path::PathBuf, process}; + +fn get_fd_from_path(path: PathBuf) -> Result { + let fd: i32 = open(path.as_path(), OFlag::O_RDONLY, Mode::empty()).map_err(|e| { + CniError::Generic(format!( + "Failed to open netns: {:?}. (fn get_fd_from_path) {}", + path, e + )) + })?; + + Ok(fd) +} + +fn get_current_thread_netns_path() -> PathBuf { + let pid: u32 = process::id(); + let tid: u64 = unsafe { libc::syscall(libc::SYS_gettid) as u64 }; + + // /proc/self/ns/net returns the namespace of the main thread, not + // of whatever thread it is running on. Make sure we use the + // thread's net namespace since the thread is switching around + PathBuf::from(format!("/proc/{}/task/{}/ns/net", pid, tid)) +} + +pub async fn exec(netns: PathBuf, f: F) -> Result +where + F: FnOnce(i32) -> Fut, + Fut: Future>, +{ + let thread_netns: PathBuf = get_current_thread_netns_path(); + let thread_netns_fd: i32 = get_fd_from_path(thread_netns)?; + let netns_fd: i32 = get_fd_from_path(netns.clone())?; + let setns_flags: CloneFlags = CloneFlags::empty(); + + // WARNING ! [Change namespace from host to container] + if let Err(e) = nix::sched::setns(netns_fd, setns_flags) { + return Err(CniError::Generic(format!( + "Failed to move namespace from host to {:?}. {}", + netns, e + ))); + } + + let res: E = f(thread_netns_fd).await?; + + // Switch back ! [Change namespace from container to host] + if let Err(e) = nix::sched::setns(thread_netns_fd, setns_flags) { + return Err(CniError::Generic(format!( + "Failed to move namespace from {:?} to host? {}", + netns, e + ))); + } + + Ok(res) +} diff --git a/network/plugins/bridge/src/route.rs b/network/plugins/bridge/src/route.rs new file mode 100644 index 00000000..9d6c47b2 --- /dev/null +++ b/network/plugins/bridge/src/route.rs @@ -0,0 +1,43 @@ +use cni_plugin::error::CniError; +use rtnetlink::Handle; +use std::{fs, net::Ipv4Addr}; + +pub async fn route_add_default(handle: &Handle, addr: Ipv4Addr) -> Result<(), CniError> { + handle + .route() + .add() + .v4() + .gateway(addr) + .execute() + .await + .map_err(|err| CniError::Generic(format!( + "[ORKANET ERROR]: Failed to route add default via {} (fn route_add_default)\n{:?}\n", + addr, err + )) + ) +} + +pub fn enable_ip_forward(is_ipv4: bool) -> Result<(), CniError> { + if is_ipv4 { + Ok(enable_ip4_forward()?) + } else { + Err(CniError::Generic( + "Cannot enable ip6 forward for the moment.".to_string(), + )) + } +} + +fn enable_ip4_forward() -> std::io::Result<()> { + echo1("/proc/sys/net/ipv4/ip_forward") +} + +fn echo1(f: &str) -> std::io::Result<()> { + if let Ok(content) = fs::read(f) { + if content.iter().map(|b| b.is_ascii_whitespace()).all(|x| x) && content == [b'1'] { + return Ok(()); + } + } + + fs::write(f, [b'1'])?; + Ok(()) +} diff --git a/network/plugins/bridge/src/types.rs b/network/plugins/bridge/src/types.rs new file mode 100644 index 00000000..218bc333 --- /dev/null +++ b/network/plugins/bridge/src/types.rs @@ -0,0 +1,43 @@ +use std::fmt; + +pub enum NetworkConfigReference { + Name, + Type, + Bridge, + IsGateway, + IsDefaultGateway, + ForceAddress, + IpMasq, + Mtu, + HairpinMode, + Ipam, + PromiscMode, + Vlan, + PreserveDefaultVlan, + VlanTrunk, + Enabledad, + Macspoofchk, +} + +impl fmt::Display for NetworkConfigReference { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + NetworkConfigReference::Name => write!(f, "name"), + NetworkConfigReference::Type => write!(f, "type"), + NetworkConfigReference::Bridge => write!(f, "bridge"), + NetworkConfigReference::IsGateway => write!(f, "isGateway"), + NetworkConfigReference::IsDefaultGateway => write!(f, "isDefaultGateway"), + NetworkConfigReference::ForceAddress => write!(f, "forceAddress"), + NetworkConfigReference::IpMasq => write!(f, "ipMasq"), + NetworkConfigReference::Mtu => write!(f, "mtu"), + NetworkConfigReference::HairpinMode => write!(f, "hairpinMode"), + NetworkConfigReference::Ipam => write!(f, "ipam"), + NetworkConfigReference::PromiscMode => write!(f, "promiscMode"), + NetworkConfigReference::Vlan => write!(f, "vlan"), + NetworkConfigReference::PreserveDefaultVlan => write!(f, "preserveDefaultVlan"), + NetworkConfigReference::VlanTrunk => write!(f, "vlanTrunk"), + NetworkConfigReference::Enabledad => write!(f, "enabledad"), + NetworkConfigReference::Macspoofchk => write!(f, "macspoofchk"), + } + } +} diff --git a/network/plugins/bridge/test/bridge.conf b/network/plugins/bridge/test/bridge.conf new file mode 100644 index 00000000..9a56b2a8 --- /dev/null +++ b/network/plugins/bridge/test/bridge.conf @@ -0,0 +1,13 @@ +{ + "cniVersion": "1.0.0", + "name": "orknet", + "type": "bridge", + "bridge": "ork0", + "isDefaultGateway": true, + "ipMasq": false, + "ipam": { + "type": "host-local", + "subnet": "10.244.0.0/24" + }, + "promiscMode": false +} \ No newline at end of file diff --git a/network/plugins/host-local/Cargo.toml b/network/plugins/host-local/Cargo.toml new file mode 100644 index 00000000..f03f8b9d --- /dev/null +++ b/network/plugins/host-local/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "host-local" +version = "0.1.0" +edition = "2021" + + +[dependencies] +cni-plugin = "0.2" +ipnet = "2.8.0" +json = "0.12.4" diff --git a/network/plugins/host-local/README.md b/network/plugins/host-local/README.md new file mode 100644 index 00000000..d6cb6150 --- /dev/null +++ b/network/plugins/host-local/README.md @@ -0,0 +1,4 @@ +# `host-local` CNI plugin + +- [cni.dev `host-local`](https://www.cni.dev/plugins/current/ipam/host-local/) +- [source code](https://github.com/containernetworking/plugins/tree/main/plugins/ipam/host-local) \ No newline at end of file diff --git a/network/plugins/host-local/host-local.conf b/network/plugins/host-local/host-local.conf new file mode 100644 index 00000000..964aedff --- /dev/null +++ b/network/plugins/host-local/host-local.conf @@ -0,0 +1,16 @@ +{ + "type": "host-local", + "ranges": [ + [ + { + "subnet": "10.244.0.0/24" + } + ] + ], + "routes": [ + { "dst": "0.0.0.0/0" } + ], + "dataDir": "/run/cni-ipam-state/orknet", + "cniVersion": "1.0.0", + "name": "Orka" +} \ No newline at end of file diff --git a/network/plugins/host-local/src/allocator.rs b/network/plugins/host-local/src/allocator.rs new file mode 100644 index 00000000..c77b555c --- /dev/null +++ b/network/plugins/host-local/src/allocator.rs @@ -0,0 +1,163 @@ + +use std::{fs::{self, File}, path::Path, io::Write, str::FromStr, net::{Ipv4Addr, IpAddr}}; +use ipnet::IpNet; +use crate::cni_error; + +pub fn get_last_address(subnet: &IpNet) -> Option { + + let last_host_opt = Iterator::max(subnet.hosts()); + last_host_opt?; + + let address = IpNet::from(last_host_opt.unwrap()); + + Some(address) +} + +pub fn find_container_id(data_dir: &String, containerid: &String, cni_version: &String) -> Option { + + let read_result = fs::read_dir(data_dir.clone()); + if let Err(error) = read_result { + cni_error::output_error( + &"Failed to read data directory".to_string(), + &error.to_string(), + cni_error::CNIErrorCode::IOFailure, + &cni_version.clone() + ); + return None; + } + + let read = read_result.unwrap(); + for path in read { + if path.is_err() { + continue; + } + let entry = path.unwrap(); + let metadata_result = entry.metadata(); + if let Err(_) = metadata_result { + continue; + } + let metadata = metadata_result.unwrap(); + if !metadata.is_file() { + continue; + } + + let os_file_name = entry.file_name(); + let opt_file_name = os_file_name.to_str(); + if opt_file_name.is_none() { + continue; + } + let file_name = opt_file_name.unwrap().to_string(); + let file_path = entry.path(); + let read_result = fs::read_to_string(file_path); + if read_result.is_err() { + continue; + } + let content = read_result.unwrap(); + if !content.eq(&containerid.clone()) { + continue; + } + + let ip_addr_result = Ipv4Addr::from_str(&file_name.clone()); + if let Err(error) = ip_addr_result { + cni_error::output_error( + &"Invalid ip address registered in data directory".to_string(), + &error.to_string(), + cni_error::CNIErrorCode::FailedToDecodeContent, + &cni_version.clone() + ); + return None; + } + + return Some(file_name.clone()); + } + + Some("".to_string())// Used to detect if the container id is registered +} + +pub fn remove_file(path: &Path, cni_version: &String) -> Result<(), ()> { + + let remove_result = fs::remove_file(path); + if let Err(error) = remove_result { + cni_error::output_error( + &"Failed to remove ip address file".to_string(), + &error.to_string(), + cni_error::CNIErrorCode::IOFailure, + &cni_version.clone() + ); + return Err(()); + } + + Ok(()) +} + +pub fn write_containerid(addr_path: &Path, containerid: &String, cni_version: &String) -> Result<(), ()> { + let create_result = File::create(addr_path.clone()); + if let Err(error) = create_result { + cni_error::output_error( + &"Failed to create registered ip address file".to_string(), + &error.to_string(), + cni_error::CNIErrorCode::IOFailure, + &cni_version.clone() + ); + return Err(()); + } + + let mut file: File = create_result.unwrap(); + let write_result = file.write_all(containerid.as_bytes()); + if let Err(error) = write_result { + cni_error::output_error( + &"Failed to write container id to ip address file".to_string(), + &error.to_string(), + cni_error::CNIErrorCode::IOFailure, + &cni_version.clone() + ); + return Err(()); + } + + Ok(()) +} + +pub fn get_new_address(data_dir: &String, subnet: &IpNet) -> Result { + + for host in subnet.hosts() { + if is_allocated_address(data_dir, &host) { + continue; + } + let ip_address = IpNet::from(host); + return Ok(ip_address); + } + + + Err(()) +} + +pub fn is_allocated_address(datadir: &String, address: &IpAddr) -> bool { + + let path: String = datadir.clone() + "/" + &address.to_string(); + if fs::metadata(path.clone()).is_err() { + return false; + } + + true +} + +pub fn init_datadir(data_dir: &String, cni_version: &String) -> Result<(), ()> { + if fs::metadata(data_dir.clone()).is_err() { + let createdir_result = fs::create_dir_all(data_dir.clone()); + match createdir_result { + Ok(()) => { + return Ok(()); + } + Err(error) => { + cni_error::output_error( + &"Failed to create dataDir folder".to_string(), + &error.to_string(), + cni_error::CNIErrorCode::IOFailure, + &cni_version.clone() + ); + return Err(()); + } + } + } + Ok(()) +} \ No newline at end of file diff --git a/network/plugins/host-local/src/cni_error.rs b/network/plugins/host-local/src/cni_error.rs new file mode 100644 index 00000000..dd04b08a --- /dev/null +++ b/network/plugins/host-local/src/cni_error.rs @@ -0,0 +1,23 @@ +use json::{object, JsonValue}; + +pub enum CNIErrorCode { + IncompatibleCNIVersion = 1, + UnsupportedFieldInNetworkConfig = 2, + ContainerUnknownOrDoesntExist = 3, + InvalidNecessaryEnvVars = 4, + IOFailure = 5, + FailedToDecodeContent = 6, + InvalidNetworkConfig = 7, + TryAgainLater = 11 +} + +pub fn output_error(msg: &String, details: &String, code: CNIErrorCode, cni_version: &String) { + let json_error: JsonValue = object!{ + cniVersion: cni_version.clone(), + code: code as u8, + msg: msg.clone(), + details: details.clone() + }; + + println!("{}", json_error); +} \ No newline at end of file diff --git a/network/plugins/host-local/src/commands/cni_add.rs b/network/plugins/host-local/src/commands/cni_add.rs new file mode 100644 index 00000000..b4c227a3 --- /dev/null +++ b/network/plugins/host-local/src/commands/cni_add.rs @@ -0,0 +1,82 @@ +use std::path::Path; + +use ipnet::IpNet; +use json::object; +use crate::types; +use crate::allocator; +use crate::cni_error; + + +pub fn exec(command: &types::CNICommand) -> Result { + let init_result = allocator::init_datadir(&command.data_dir.clone(), &command.cni_version.clone()); + if let Err(()) = init_result { + return Err(()); + } + + let find_result = allocator::find_container_id(&command.data_dir.clone(), &command.container_id.clone(), &command.cni_version.clone()); + if find_result.is_none() { + return Err(()); + } + let already_exists = !find_result.unwrap().eq(""); + if already_exists { + cni_error::output_error( + &"This containerid is already registered".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNecessaryEnvVars, + &command.cni_version.clone() + ); + return Err(()); + } + + let subnet = command.subnet; + + let get_result = allocator::get_new_address(&command.data_dir.clone(), &subnet.clone()); + if let Err(()) = get_result { + return Err(()); + } + let new_addr: IpNet = get_result.unwrap(); + let new_addr_str: String = new_addr.addr().to_string(); + let new_addr_path_str = command.data_dir.clone() + "/" + &new_addr_str; + let new_addr_path = Path::new(&new_addr_path_str); + + let write_result = allocator::write_containerid(new_addr_path, &command.container_id.clone(), &command.cni_version.clone()); + if let Err(()) = write_result { + return Err(()); + } + + let success_result = get_success_result(&new_addr, command); + if let Err(()) = success_result { + return Err(()); + } + let success = success_result.unwrap(); + + Ok(success) +} + +fn get_success_result(new_addr: &IpNet, command: &types::CNICommand) -> Result { + + let gateway_opt = allocator::get_last_address(&command.subnet.clone()); + if gateway_opt.is_none() { + return Err(()); + } + let gateway = gateway_opt.unwrap(); + + let result = object! { + cni_version: command.cni_version.clone(), + ips: [ + object! { + version: "4", + address: new_addr.addr().to_string(), + gateway: gateway.addr().to_string() + } + ], + routes: [ + "0.0.0.0/0" + ], + dns: object!{ + nameservers: [ "8.8.8.8", "8.8.4.4" ] + } + }; + + Ok(result.to_string()) +} \ No newline at end of file diff --git a/network/plugins/host-local/src/commands/cni_check.rs b/network/plugins/host-local/src/commands/cni_check.rs new file mode 100644 index 00000000..c8b8982f --- /dev/null +++ b/network/plugins/host-local/src/commands/cni_check.rs @@ -0,0 +1,34 @@ +use crate::cni_error; +use crate::types; +use crate::allocator; + +pub fn exec(command: &types::CNICommand) -> Result<(), ()> { + + let init_result = allocator::init_datadir(&command.data_dir.clone(), &command.cni_version.clone()); + if let Err(()) = init_result { + return Err(()); + } + + let find_result = allocator::find_container_id( + &command.data_dir.clone(), + &command.container_id.clone(), + &command.cni_version.clone() + ); + + if find_result.is_none() { + return Err(()); + } + + let container_ip = find_result.unwrap(); + if container_ip.is_empty() { + cni_error::output_error( + &"This container does not have registered ip address".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::ContainerUnknownOrDoesntExist, + &command.cni_version.clone() + ); + return Err(()); + } + + Ok(()) +} \ No newline at end of file diff --git a/network/plugins/host-local/src/commands/cni_del.rs b/network/plugins/host-local/src/commands/cni_del.rs new file mode 100644 index 00000000..6432563a --- /dev/null +++ b/network/plugins/host-local/src/commands/cni_del.rs @@ -0,0 +1,30 @@ +use std::path::Path; + +use crate::types; +use crate::allocator; + + +pub fn exec(command: &types::CNICommand) -> Result<(), ()> { + let init_result = allocator::init_datadir(&command.data_dir.clone(), &command.cni_version.clone()); + if let Err(()) = init_result { + return Err(()); + } + + let find_result = allocator::find_container_id(&command.data_dir.clone(), &command.container_id.clone(), &command.cni_version.clone()); + if find_result.is_none() { + return Err(()); + } + + let data_dir = command.data_dir.clone(); + let ip_addr = find_result.unwrap(); + let file_path_str = data_dir + "/" + &ip_addr; + let file_path = Path::new(&file_path_str); + + let remove_result = allocator::remove_file(file_path, &command.cni_version.clone()); + if let Err(()) = remove_result { + return Err(()); + } + + Ok(()) +} + diff --git a/network/plugins/host-local/src/config.rs b/network/plugins/host-local/src/config.rs new file mode 100644 index 00000000..004cf39f --- /dev/null +++ b/network/plugins/host-local/src/config.rs @@ -0,0 +1,135 @@ +use std::str::FromStr; + +use cni_plugin::config::NetworkConfig; +use ipnet::IpNet; + +use crate::cni_error; + +pub fn get_datadir_from_config(cni_version: &String, config: &NetworkConfig) -> Result { + let get_opt = config.specific.get("dataDir"); + if get_opt.is_none() { + cni_error::output_error( + &"No dataDir field in config file".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + &cni_version.clone() + ); + return Err(()); + } + + let data_dir_value = get_opt.unwrap(); + let parse_opt = data_dir_value.as_str(); + if parse_opt.is_none() { + cni_error::output_error( + &"Invalid dataDir field in config file".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + &cni_version.clone() + ); + return Err(()); + } + + let data_dir: String = parse_opt.unwrap().to_string(); + Ok(data_dir) +} + + +pub fn get_subnet_from_config(cni_version: &String, config: &NetworkConfig) -> Option { + + let ranges_value = config.specific.get("ranges"); + + if ranges_value.is_none() { + cni_error::output_error( + &"Cannot get ranges field from config file".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + cni_version + ); + return None; + } + + let ranges_array = ranges_value.unwrap().as_array(); + if ranges_array.is_none() { + cni_error::output_error( + &"Invalid ranges field in config file".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + cni_version + ); + return None; + } + + let subnet_array_value = ranges_array.unwrap().first(); + if subnet_array_value.is_none() { + cni_error::output_error( + &"Cannot get first array of subnet in config file".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + cni_version + ); + return None; + } + + let subnet_array = subnet_array_value.unwrap().as_array(); + if subnet_array.is_none() { + cni_error::output_error( + &"Invalid array of subnet in config file".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + cni_version + ); + return None; + } + + let subnet_obj = subnet_array.unwrap().first(); + if subnet_obj.is_none() { + cni_error::output_error( + &"Cannot get subnet element object in config file".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + cni_version + ); + return None; + } + + let subnet = subnet_obj.unwrap().get("subnet"); + if subnet.is_none() { + cni_error::output_error( + &"Cannot get subnet in subnet object in config file".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + cni_version + ); + return None; + } + + let subnet_str = subnet.unwrap().as_str(); + if subnet_str.is_none() { + cni_error::output_error( + &"Invalid subnet string provided in config file".to_string(), + &"".to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + cni_version + ); + return None; + } + + let subnet: String = subnet_str.unwrap().to_string(); + let from_str_result = IpNet::from_str(&subnet); + if let Err(error) = from_str_result { + cni_error::output_error( + &"Invalid subnet format provided in config file".to_string(), + &error.to_string(), + cni_error::CNIErrorCode::InvalidNetworkConfig, + cni_version + ); + return None; + } + + Some(from_str_result.unwrap()) +} + +pub fn get_cni_version_from_config(config: &NetworkConfig) -> String { + + config.cni_version.to_string() +} \ No newline at end of file diff --git a/network/plugins/host-local/src/main.rs b/network/plugins/host-local/src/main.rs new file mode 100644 index 00000000..c9894a1d --- /dev/null +++ b/network/plugins/host-local/src/main.rs @@ -0,0 +1,104 @@ +pub mod allocator; +pub mod cni_error; +pub mod config; +pub mod types; +pub mod commands { + pub mod cni_add; + pub mod cni_del; + pub mod cni_check; +} + + +use cni_plugin::Cni; +use ipnet::IpNet; + +fn main() { + match Cni::load() { + Cni::Add { container_id, ifname, netns, path: _, config } => { + let cni_version = config::get_cni_version_from_config(&config); + + let subnet_opt: Option = config::get_subnet_from_config(&cni_version.clone(), &config); + if subnet_opt.is_none() { + return; + } + let get_result = config::get_datadir_from_config(&cni_version.clone(), &config); + if let Err(()) = get_result { + return; + } + let data_dir = get_result.unwrap(); + + let command = types::CNICommand { + container_id, + ifname, + netns: netns.to_str().unwrap().to_string(), + data_dir, + subnet: subnet_opt.unwrap(), + cni_version + }; + + let alloc_result = commands::cni_add::exec(&command); + if let Ok(result) = alloc_result { + print!("{}", result); + } + } + Cni::Del { container_id, ifname, netns, path: _, config } => { + let cni_version = config::get_cni_version_from_config(&config); + + let subnet_opt: Option = config::get_subnet_from_config(&cni_version.clone(), &config); + let get_result = config::get_datadir_from_config(&cni_version.clone(), &config); + if let Err(()) = get_result { + return; + } + let data_dir = get_result.unwrap(); + + let mut netns_value = "".to_string(); + if let Some(pathbuf) = netns { + netns_value = pathbuf.to_str().unwrap().to_string(); + } + + let command = types::CNICommand { + container_id, + ifname, + netns: netns_value, + data_dir, + subnet: subnet_opt.unwrap(), + cni_version + }; + + let cmd_result = commands::cni_del::exec(&command); + if let Err(()) = cmd_result { + } + } + Cni::Check { container_id, ifname, netns, path: _, config } => { + let cni_version = config::get_cni_version_from_config(&config); + + let subnet_opt: Option = config::get_subnet_from_config(&cni_version.clone(), &config); + if subnet_opt.is_none() { + return; + } + + let get_result = config::get_datadir_from_config(&cni_version.clone(), &config); + if let Err(()) = get_result { + return; + } + let data_dir = get_result.unwrap(); + + let command = types::CNICommand { + container_id, + ifname, + netns: netns.to_str().unwrap().to_string(), + data_dir, + subnet: subnet_opt.unwrap(), + cni_version + }; + + let cmd_result = commands::cni_check::exec(&command); + if let Err(()) = cmd_result { + } + + } + Cni::Version(_) => unreachable!() + } + +} + diff --git a/network/plugins/host-local/src/types.rs b/network/plugins/host-local/src/types.rs new file mode 100644 index 00000000..ee3b5059 --- /dev/null +++ b/network/plugins/host-local/src/types.rs @@ -0,0 +1,11 @@ +use ipnet::IpNet; + +pub struct CNICommand { + pub container_id: String, + pub ifname: String, + pub netns: String, + + pub data_dir: String, + pub subnet: IpNet, + pub cni_version: String +} \ No newline at end of file diff --git a/network/plugins/orka-cni/10-orknet.conf b/network/plugins/orka-cni/10-orknet.conf new file mode 100644 index 00000000..52b0db6b --- /dev/null +++ b/network/plugins/orka-cni/10-orknet.conf @@ -0,0 +1 @@ +{"cniVersion":"1.0.0","name":"orknet","type":"orka-cni","bridge":"ork0","subnet":"10.244.0.0/24"} \ No newline at end of file diff --git a/network/plugins/orka-cni/Cargo.toml b/network/plugins/orka-cni/Cargo.toml new file mode 100644 index 00000000..aa974714 --- /dev/null +++ b/network/plugins/orka-cni/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "orka-cni" +version = "0.1.0" +edition = "2021" + +[dependencies] +cni-plugin = { version = "0.2", features = ["with-tokio"] } +tokio = { version = "1.32", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +log = "0.4" +which = "4.4.0" diff --git a/network/plugins/orka-cni/Makefile b/network/plugins/orka-cni/Makefile new file mode 100644 index 00000000..b5a07b96 --- /dev/null +++ b/network/plugins/orka-cni/Makefile @@ -0,0 +1,34 @@ +PLUGINS_PATH='./target/debug:./vendors' +NET_CONF_PATH='.' +CNITOOL_PATH='$(HOME)/go/bin/cnitool' +BUILT_BIN_PATH='./target/debug/orka-cni' + +NETWORK_NAME='orknet' +NS_NAME='testing' + +.cargo-build: + @cargo build + +add: .cargo-build + @sudo CNI_PATH=$(PLUGINS_PATH) NETCONFPATH=$(NET_CONF_PATH) $(CNITOOL_PATH) add $(NETWORK_NAME) /var/run/netns/$(NS_NAME) + @echo '' + +del: .cargo-build + @sudo CNI_PATH=$(PLUGINS_PATH) NETCONFPATH=$(NET_CONF_PATH) $(CNITOOL_PATH) del $(NETWORK_NAME) /var/run/netns/$(NS_NAME) + @echo '' + +check: .cargo-build + @sudo CNI_PATH=$(PLUGINS_PATH) NETCONFPATH=$(NET_CONF_PATH) $(CNITOOL_PATH) check $(NETWORK_NAME) /var/run/netns/$(NS_NAME) + @echo '' + +version: .cargo-build + @echo '{"cniVersion": "1.0.0", "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0" ]}' | sudo CNI_COMMAND=VERSION $(BUILT_BIN_PATH) + @echo '' + +download_vendors: + @mkdir -p ./vendors + @wget -O ./vendors/vendors.tgz https://github.com/containernetworking/plugins/releases/download/v1.3.0/cni-plugins-linux-amd64-v1.3.0.tgz + @tar -xvf ./vendors/vendors.tgz -C ./vendors ./host-local + @rm ./vendors/vendors.tgz + @cd ../bridge && cargo build + @cp ../bridge/target/debug/bridge ./vendors/bridge diff --git a/network/plugins/orka-cni/src/delegate_conf.rs b/network/plugins/orka-cni/src/delegate_conf.rs new file mode 100644 index 00000000..14c6899f --- /dev/null +++ b/network/plugins/orka-cni/src/delegate_conf.rs @@ -0,0 +1,50 @@ +use crate::plugins::PluginsBin::{Bridge, HostLocal}; +use cni_plugin::config::{IpamConfig, NetworkConfig}; +use cni_plugin::error::CniError; +use serde_json::Value; +use std::collections::HashMap; + +pub fn create_delegation_config(parent_config: NetworkConfig) -> Result { + let NetworkConfig { + cni_version, + name, + args, + prev_result, + runtime, + .. + } = parent_config; + + let bridge_name = parent_config + .specific + .get("bridge") + .ok_or(CniError::MissingField("bridge"))? + .clone(); + + let subnet = parent_config + .specific + .get("subnet") + .ok_or(CniError::MissingField("subnet"))? + .clone(); + + Ok(NetworkConfig { + cni_version, + name, + args, + prev_result, + runtime, + // bridge delegate plugin + plugin: Bridge.to_string(), + specific: HashMap::from([ + ("bridge".to_string(), bridge_name), + // ("isGateway".to_string(), Value::Bool(true)), + ("isDefaultGateway".to_string(), Value::Bool(true)), + ]), + ip_masq: false, + ipam: Some(IpamConfig { + plugin: HostLocal.to_string(), + specific: HashMap::from([("subnet".to_string(), subnet)]), + }), + // Not used by the bridge plugin + dns: None, + }) +} diff --git a/network/plugins/orka-cni/src/delegation.rs b/network/plugins/orka-cni/src/delegation.rs new file mode 100644 index 00000000..f54f7524 --- /dev/null +++ b/network/plugins/orka-cni/src/delegation.rs @@ -0,0 +1,153 @@ +use cni_plugin::{config::NetworkConfig, error::CniError, reply::ReplyPayload, Command}; +use log::{debug, error, info}; +use std::io::ErrorKind; +use std::{ + env, + io::Cursor, + path::Path, + process::{ExitStatus, Stdio}, +}; +use tokio::process; +use which::which_in; + +/// Inspired by [cni_plugin::delegation::delegate](https://docs.rs/cni-plugin/latest/cni_plugin/delegation/fn.delegate.html) +/// +/// We removed the `unwrap` and the assertion that can cause `panic`. +pub async fn delegate( + sub_plugin: &str, + command: Command, + config: &NetworkConfig, +) -> Result +where + S: for<'de> ReplyPayload<'de>, +{ + let cwd = env::current_dir().map_err(|_| CniError::NoCwd)?; + let plugin = which_in( + sub_plugin, + Some(env::var("CNI_PATH").map_err(|err| CniError::MissingEnv { + var: "CNI_PATH", + err, + })?), + cwd, + ) + .map_err(|err| CniError::MissingPlugin { + name: sub_plugin.into(), + err, + })?; + + // convert network config into bytes + let config_bytes = serde_json::to_vec(config).map_err(|err| CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err.into()), + })?; + + match delegate_command(&plugin, command, &config_bytes).await { + Ok((status, stdout)) => { + if stdout.is_empty() { + if matches!(command, Command::Add) { + delegate_command(&plugin, Command::Del, &config_bytes) + .await + .map_err(|err| CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err), + })?; + } + + return Err(CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(CniError::MissingOutput), + }); + } + + if status.success() { + let reader = Cursor::new(stdout); + Ok( + serde_json::from_reader(reader).map_err(|err| CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err.into()), + })?, + ) + } else { + if matches!(command, Command::Add) { + delegate_command(&plugin, Command::Del, &config_bytes) + .await + .map_err(|err| CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err), + })?; + } + + Err(CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(CniError::Generic(String::from_utf8_lossy(&stdout).into())), + }) + } + } + Err(err) => { + error!("error running delegate: {}", err); + if matches!(command, Command::Add) { + // We're already failing pretty badly so this is a Just In Case, but + // in all likelihood won't work either. So we ignore any failure. + delegate_command(&plugin, Command::Del, &config_bytes) + .await + .ok(); + } + + Err(CniError::Delegated { + plugin: sub_plugin.into(), + err: Box::new(err), + }) + } + } +} + +async fn delegate_command( + plugin: impl AsRef, + command: impl AsRef, + mut stdin_bytes: &[u8], +) -> Result<(ExitStatus, Vec), CniError> { + let plugin = plugin.as_ref(); + let command = command.as_ref(); + + info!( + "delegating to plugin at {} for command={}", + plugin.display(), + command + ); + + debug!("spawing child process, async=tokio"); + let mut child = process::Command::new(plugin) + .env("CNI_COMMAND", command) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn()?; + + { + debug!("taking child stdin"); + let mut stdin = child.stdin.take().ok_or(std::io::Error::new( + ErrorKind::Other, + "child stdin not found", + ))?; + + debug!("copying bytes={} to stdin", stdin_bytes.len()); + + debug!("before config_len = {:?}", stdin_bytes); + tokio::io::copy_buf(&mut stdin_bytes, &mut stdin).await?; + debug!("after config_len = {:?}", stdin_bytes); + + debug!("dropping stdin handle"); + } + + debug!("awaiting child"); + let output = child.wait_with_output().await?; + + info!( + "delegate plugin at {} for command={} has returned with {}; stdout bytes={}", + plugin.display(), + command, + output.status, + output.stdout.len() + ); + Ok((output.status, output.stdout)) +} diff --git a/network/plugins/orka-cni/src/lib.rs b/network/plugins/orka-cni/src/lib.rs new file mode 100644 index 00000000..b06a4e7e --- /dev/null +++ b/network/plugins/orka-cni/src/lib.rs @@ -0,0 +1,43 @@ +mod delegate_conf; +mod delegation; +mod plugins; + +use crate::delegate_conf::create_delegation_config; +use crate::delegation::delegate; +use crate::plugins::PluginsBin::Bridge; +use cni_plugin::config::NetworkConfig; +use cni_plugin::reply::{reply, ErrorReply, SuccessReply}; +use cni_plugin::{Cni, Command}; + +pub async fn run() { + let result: Option> = match Cni::load() { + Cni::Add { config, .. } => Some(command_handler(Command::Add, config).await), + Cni::Del { config, .. } => Some(command_handler(Command::Del, config).await), + Cni::Check { config, .. } => Some(command_handler(Command::Check, config).await), + // already included with `load()` method + Cni::Version(_) => None, + }; + + match result { + Some(Ok(success)) => reply(success), + Some(Err(error)) => reply(error), + _ => {} + }; +} + +/// Generic command handler +/// (only the command needs to change for now) +async fn command_handler<'a>( + command: Command, + config: NetworkConfig, +) -> Result> { + let cni_version = config.cni_version.clone(); + + delegate::( + &Bridge.to_string(), + command, + &create_delegation_config(config).map_err(|e| e.into_reply(cni_version.clone()))?, + ) + .await + .map_err(|e| e.into_reply(cni_version)) +} diff --git a/network/plugins/orka-cni/src/main.rs b/network/plugins/orka-cni/src/main.rs new file mode 100644 index 00000000..8052f2df --- /dev/null +++ b/network/plugins/orka-cni/src/main.rs @@ -0,0 +1,10 @@ +use cni_plugin::logger; +use tokio::runtime::Runtime; + +fn main() { + logger::install(env!("CARGO_PKG_NAME")); + + if let Ok(runtime) = Runtime::new() { + runtime.block_on(async move { orka_cni::run().await }); + }; +} diff --git a/network/plugins/orka-cni/src/plugins.rs b/network/plugins/orka-cni/src/plugins.rs new file mode 100644 index 00000000..ab4e2ea6 --- /dev/null +++ b/network/plugins/orka-cni/src/plugins.rs @@ -0,0 +1,19 @@ +use std::fmt::{Display, Formatter}; + +#[allow(dead_code)] +pub enum PluginsBin { + OrkaCni, + Bridge, + HostLocal, +} + +impl Display for PluginsBin { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let str = match self { + PluginsBin::OrkaCni => "orka-cni", + PluginsBin::Bridge => "bridge", + PluginsBin::HostLocal => "host-local", + }; + write!(f, "{}", str) + } +}