base commit

This commit is contained in:
Guillermo Roche
2025-05-26 20:45:07 +02:00
commit 1394b5d76c
30 changed files with 3651 additions and 0 deletions

32
net-logger/Cargo.toml Normal file
View File

@@ -0,0 +1,32 @@
[package]
name = "net-logger"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
aya = "0.12"
aya-log = "0.2"
clap = { version = "4.1", features = ["derive"] }
net-logger-common = { path = "../net-logger-common", features = ["user"] }
anyhow = "1"
env_logger = "0.10"
libc = "0.2"
log = "0.4"
tokio = { version = "1.25", features = [
"macros",
"rt",
"rt-multi-thread",
"net",
"signal",
] }
bytes = "*"
network-types = "0.0.6"
elasticsearch = "8.17.0-alpha.1"
serde_json = "*"
serde = "*"
chrono = "*"
[[bin]]
name = "net-logger"
path = "src/main.rs"

View File

@@ -0,0 +1,252 @@
use std::{
collections::HashMap,
net::{Ipv4Addr, Ipv6Addr},
time::SystemTime,
};
use crate::utils::ip::print_proto;
use network_types::ip::{IpProto, Ipv4Hdr};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Serialize, Deserialize)]
pub struct ConectionCoreAtom {
pub len: u128,
pub ip4_list: Vec<IpAtom<Ipv4Addr>>,
pub ip6_list: Vec<IpAtom<Ipv6Addr>>,
}
#[derive(Serialize, Deserialize)]
pub struct IpAtom<IpType> {
len: u128,
ip_local: IpType,
port_local: u16,
ip_remote: IpType,
port_remote: u16,
//pub proto_len: HashMap<u8, u128>
pub proto_len: Vec<(u8, u128)>,
}
impl ConectionCoreAtom {
pub fn new() -> Self {
ConectionCoreAtom {
len: 0,
ip4_list: Vec::new(),
ip6_list: Vec::new(),
}
}
pub fn addv4(
&mut self,
source_ip: u32,
dest_ip: u32,
source_port: u16,
dest_port: u16,
protocol: IpProto,
size: u16,
) {
self.len += size as u128;
let source_ip_format = Ipv4Addr::from_bits(source_ip);
let dest_ip_format = Ipv4Addr::from_bits(dest_ip);
let local_ip;
let remote_ip;
let local_port;
let remote_port;
if source_ip_format.is_private() {
local_ip = source_ip_format;
remote_ip = dest_ip_format;
local_port = source_port;
remote_port = dest_port;
} else {
local_ip = dest_ip_format;
remote_ip = source_ip_format;
local_port = dest_port;
remote_port = dest_port;
}
match self.check_packagev4(local_ip, remote_ip, local_port, remote_port) {
Some(ip_atom) => {
ip_atom.add(protocol, size as u128);
}
None => {
let mut ip_atom = IpAtom::new_ip(local_ip, remote_ip, local_port, remote_port);
ip_atom.add(protocol, size as u128);
self.ip4_list.push(ip_atom);
}
}
}
pub fn addv6(
&mut self,
source_ip: Ipv6Addr,
dest_ip: Ipv6Addr,
source_port: u16,
dest_port: u16,
protocol: IpProto,
size: u16,
) {
self.len += size as u128;
let local_ip;
let remote_ip;
let local_port;
let remote_port;
if source_ip.is_unique_local() {
local_ip = source_ip;
local_port = source_port;
remote_ip = dest_ip;
remote_port = dest_port;
} else {
local_ip = dest_ip;
local_port = dest_port;
remote_ip = source_ip;
remote_port = source_port;
}
match self.check_packagev6(local_ip, remote_ip, local_port, remote_port) {
Some(ip_atom) => {
ip_atom.add(protocol, size as u128);
}
None => {
let mut ip_atom = IpAtom::new_ip(local_ip, remote_ip, local_port, remote_port);
ip_atom.add(protocol, size as u128);
self.ip6_list.push(ip_atom);
}
}
}
fn check_packagev4(
&mut self,
local_ip: Ipv4Addr,
remote_ip: Ipv4Addr,
local_port: u16,
remote_port: u16,
) -> Option<&mut IpAtom<Ipv4Addr>> {
let mut index = 0;
loop {
match self.ip4_list.get(index) {
Some(ip) => {
if ip.eq_splited(local_ip, remote_ip, local_port, remote_port) {
return self.ip4_list.get_mut(index);
}
}
None => {
return None;
}
}
index += 1;
}
}
fn check_packagev6(
&mut self,
local_ip: Ipv6Addr,
remote_ip: Ipv6Addr,
local_port: u16,
remote_port: u16,
) -> Option<&mut IpAtom<Ipv6Addr>> {
let index = 0;
loop {
match self.ip6_list.get(index) {
Some(ip) => {
if ip.eq_splited(local_ip, remote_ip, local_port, remote_port) {
return self.ip6_list.get_mut(index);
}
}
None => return None,
}
}
}
pub fn generate_json(&self, timestamp: String) -> Value {
json!({
"@timestamp" : timestamp,
"ips_v4" : self.ip4_list,
"ips_v6" : self.ip6_list,
})
}
pub fn reset(&mut self) -> Self {
let old_len = self.len;
self.len = 0;
Self {
len: old_len,
ip4_list: self.ip4_list.drain(..).collect(),
ip6_list: self.ip6_list.drain(..).collect(),
}
}
/*fn create_ipv4(&mut self, source_ip: u32, dest_ip: u32) {
let source_ip_format = Ipv4Addr::from_bits(source_ip);
let dest_ip_format = Ipv4Addr::from_bits(dest_ip);
self.ip4_list.push(if source_ip_format.is_private() {
IpAtom::new_ip(source_ip_format, dest_ip_format)
} else {
IpAtom::new_ip(dest_ip_format, source_ip_format)
})
}
fn create_ipv6(&mut self, source_ip: Ipv6Addr, dest_ip: Ipv6Addr) {
self.ip6_list.push(if source_ip.is_unique_local() {
IpAtom::new_ip(source_ip, dest_ip)
} else {
IpAtom::new_ip(dest_ip, source_ip)
});
}*/
}
impl<IpType: Eq> IpAtom<IpType> {
pub fn new_ip(local_ip: IpType, remote_ip: IpType, local_port: u16, remote_port: u16) -> Self {
IpAtom {
len: 0,
ip_local: local_ip,
port_local: local_port,
ip_remote: remote_ip,
port_remote: remote_port,
proto_len: Vec::new(),
}
}
//hashmap can be less efficient
/*pub fn add(&mut self,protocol: IpProto, size: u128) {
self.len+=size;
match self.proto_len.get_mut(&(protocol as u8)) {
Some(l) => {
*l+=size;
},
None => {
self.proto_len.insert(protocol as u8,size);
},
};
}*/
pub fn add(&mut self, protocol: IpProto, size: u128) {
self.len += size;
let mut index = 0;
loop {
match self.proto_len.get_mut(index) {
Some(proto) => {
if proto.0 == protocol as u8 {
proto.1 += size;
}
}
None => {
self.proto_len.push((protocol as u8, size));
break;
}
}
index += 1;
}
}
pub fn eq_splited(
&self,
local_ip: IpType,
remote_ip: IpType,
local_port: u16,
remote_port: u16,
) -> bool {
((self.ip_local == local_ip && self.port_local == local_port)
&& (self.ip_remote == remote_ip && self.port_remote == remote_port))
|| ((self.ip_local == remote_ip && self.port_local == remote_port)
&& (self.ip_remote == local_ip && self.port_remote == local_port))
}
}

View File

@@ -0,0 +1,50 @@
use std::net::{Ipv4Addr, Ipv6Addr};
pub struct Ip4Wrapper {
raw: Ipv4Addr,
}
pub struct Ip6Wrapper {
raw: Ipv6Addr,
}
pub trait IpWrapper<IpType> {
fn is_private(&self) -> bool;
fn get_raw(&self) -> IpType;
}
impl Ip4Wrapper {
pub fn new(ip: u32) -> Self {
Self {
raw: Ipv4Addr::from_bits(ip),
}
}
}
impl Ip6Wrapper {
pub fn new(ip: Ipv6Addr) -> Self {
Self {
raw: ip,
}
}
}
impl IpWrapper<Ipv4Addr> for Ip4Wrapper {
fn is_private(&self) -> bool {
self.raw.is_private()
}
fn get_raw(&self) -> Ipv4Addr {
self.raw
}
}
impl IpWrapper<Ipv6Addr> for Ip6Wrapper {
fn is_private(&self) -> bool {
self.raw.is_unique_local()
}
fn get_raw(&self) -> Ipv6Addr {
self.raw
}
}

View File

@@ -0,0 +1,3 @@
pub mod ip_group;
mod ip_wrapper;
pub mod store;

View File

@@ -0,0 +1,96 @@
use network_types::ip::IpProto;
use crate::elk::elasticsearch::ElasticConection;
use super::ip_group::ConectionCoreAtom;
use std::{
future::IntoFuture,
net::Ipv6Addr,
time::{Duration, SystemTime, UNIX_EPOCH},
};
pub struct net_stats_storage {
connections_store: ConectionCoreAtom,
elastic_connection: ElasticConection,
old_connections: Vec<ConectionCoreAtom>,
last_insert: SystemTime,
}
impl net_stats_storage {
pub fn new() -> Self {
Self {
connections_store: ConectionCoreAtom::new(),
elastic_connection: ElasticConection::new().unwrap(),
old_connections: Vec::new(),
last_insert: SystemTime::now(),
}
}
pub async fn addv4(
&mut self,
source_ip: u32,
dest_ip: u32,
source_port: u16,
dest_port: u16,
protocol: IpProto,
size: u16,
) {
self.store_or_not().await;
self.connections_store
.addv4(source_ip, dest_ip, source_port, dest_port, protocol, size);
}
pub async fn addv6(
&mut self,
source_ip: Ipv6Addr,
dest_ip: Ipv6Addr,
source_port: u16,
dest_port: u16,
protocol: IpProto,
size: u16,
) {
self.store_or_not().await;
self.connections_store
.addv6(source_ip, dest_ip, source_port, dest_port, protocol, size);
}
pub async fn store_or_not(&mut self) {
if self
.last_insert
.duration_since(SystemTime::now())
.unwrap_or(Duration::from_secs(2))
.as_secs()
> 2
|| self.connections_store.len > 200000
{
self.store_in_elastic().await;
}
}
pub async fn store_in_elastic(&mut self) {
self.old_connections.push(self.connections_store.reset());
self.connections_store = ConectionCoreAtom::new();
println!("entra:{}", self.old_connections.len());
for con in &self.old_connections {
println!(
"datos:{}",
chrono::offset::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
);
match self
.elastic_connection
.send(con.generate_json(
chrono::offset::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
))
.await
{
Ok(_) => continue,
Err(_) => {
println!("No va");
break;
}
}
}
}
}

View File

@@ -0,0 +1,71 @@
use elasticsearch::auth::Credentials;
use elasticsearch::http::transport::SingleNodeConnectionPool;
use elasticsearch::http::transport::TransportBuilder;
use elasticsearch::http::Url;
use elasticsearch::Elasticsearch;
use elasticsearch::IndexParts;
use serde_json::Value;
use std::fmt;
pub struct ElasticConection {
conection: Elasticsearch,
}
pub struct ElasticConErr {
content: String,
}
impl ElasticConection {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let url = Url::parse("http://elastic_ip:9200")?;
let credentials = Credentials::Basic("elastic".into(), "password".into());
let connection_pool = SingleNodeConnectionPool::new(url);
let transport = TransportBuilder::new(connection_pool)
.auth(credentials)
.build()?;
Ok(Self {
conection: Elasticsearch::new(transport),
})
}
pub async fn send(&self, data: Value) -> Result<(), ElasticConErr> {
println!("aquí tambi'en entra");
let raw_response = self
.conection
.index(IndexParts::Index("netlogger-0.1"))
.body(data)
.send()
.await;
let response = match raw_response {
Ok(r) => r,
Err(e) => {
return Err(ElasticConErr {
content: e.to_string(),
})
}
};
let status_code = response.status_code();
match status_code.clone().is_success() {
true => Ok(()),
false => {
let err_ret = match response.text().await {
Ok(ret) => ret,
Err(_e) => status_code.as_str().to_string(),
};
Err(ElasticConErr { content: err_ret })
}
}
}
}
impl fmt::Display for ElasticConErr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.content)
}
}
impl fmt::Debug for ElasticConErr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.content)
}
}

View File

@@ -0,0 +1 @@
pub mod elasticsearch;

159
net-logger/src/main.rs Normal file
View File

@@ -0,0 +1,159 @@
use anyhow::Context;
use aya::maps::AsyncPerfEventArray;
use aya::programs::{Xdp, XdpFlags};
use aya::util::online_cpus;
use aya::{include_bytes_aligned, Bpf};
use aya_log::BpfLogger;
use aya_log::Formatter;
use bytes::BytesMut;
use clap::Parser;
use log::{debug, info, warn};
use net_logger_common::Event;
use network_types::eth::EtherType;
use tokio::signal;
mod clasificator;
mod elk;
mod utils;
#[derive(Debug, Parser)]
struct Opt {
#[clap(short, long, default_value = "lo")]
iface: String,
}
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
env_logger::init();
// Bump the memlock rlimit. This is needed for older kernels that don't use the
// new memcg based accounting, see https://lwn.net/Articles/837122/
let rlim = libc::rlimit {
rlim_cur: libc::RLIM_INFINITY,
rlim_max: libc::RLIM_INFINITY,
};
let ret = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &rlim) };
if ret != 0 {
debug!("remove limit on locked memory failed, ret is: {}", ret);
}
if let Err(e) = load_bpf().await {
eprintln!("error: {:#}", e);
}
Ok(())
}
async fn load_bpf() -> Result<(), aya::BpfError> {
// This will include your eBPF object file as raw bytes at compile-time and load it at
// runtime. This approach is recommended for most real-world use cases. If you would
// like to specify the eBPF program at runtime rather than at compile-time, you can
// reach for `Bpf::load_file` instead.
#[cfg(debug_assertions)]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/debug/net-logger"
))?;
#[cfg(not(debug_assertions))]
let mut bpf = Bpf::load(include_bytes_aligned!(
"../../target/bpfel-unknown-none/release/net-logger"
))?;
if let Err(e) = BpfLogger::init(&mut bpf) {
// This can happen if you remove all log statements from your eBPF program.
warn!("failed to initialize eBPF logger: {}", e);
}
proces_events_xdp(bpf).await
}
async fn proces_events_xdp(mut bpf: Bpf) -> Result<(), aya::BpfError> {
let opt = Opt::parse();
// This will include your eBPF object file as raw bytes at compile-time and load it at
// runtime. This approach is recommended for most real-world use cases. If you would
// like to specify the eBPF program at runtime rather than at compile-time, you can
// reach for `Bpf::load_file` instead.
//let program : &mut SockOps = bpf.program_mut("get_package_data").unwrap().try_into().unwrap();
//program.load().unwrap();
let program: &mut Xdp = bpf.program_mut("net_logger").unwrap().try_into().unwrap();
program.load().unwrap();
program.attach(&opt.iface, XdpFlags::default())
.context("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE").unwrap();
let cpus = online_cpus().unwrap();
let num_cpus = cpus.len();
//let events_raw = bpf.map_mut("EVENTS");
let mut events: AsyncPerfEventArray<_> = bpf
.take_map("EVENTS")
.context("failed to map QUERY_RING")
.unwrap()
.try_into()
.unwrap();
for cpu in cpus {
let mut buf = events.open(cpu, None).unwrap();
tokio::task::spawn(async move {
let mut buffers = (0..num_cpus)
.map(|_| BytesMut::with_capacity(10240))
.collect::<Vec<_>>();
let mut con = clasificator::store::net_stats_storage::new();
loop {
let events = buf.read_events(&mut buffers).await.unwrap();
for i in 0..events.read {
// get timestamp
//let now = Local::now();
// read the event
let buf = &mut buffers[i];
let ptr = buf.as_ptr() as *const Event;
let data = unsafe { ptr.read_unaligned() };
// parse out the data
match data.ipv {
EtherType::Ipv4 => {
println!("source ip:{}, source port:{}, dest ip:{}, dest port:{}, proto:{}, size: {}",
aya_log::Ipv4Formatter::format(data.source_ipv4),
data.source_port,
aya_log::Ipv4Formatter::format(data.dest_ipv4),
data.dest_port,
utils::ip::print_proto(data.proto),
data.len);
con.addv4(
data.source_ipv4,
data.dest_ipv4,
data.source_port,
data.dest_port,
data.proto,
data.len,
)
.await;
//println!("package size:{}, ips:{}", con.len, con.ip4_list.get(con.ip4_list.len()-1).unwrap().proto_len.get(0).unwrap_or(&(0,0)).1);
}
EtherType::Ipv6 => {
println!("source ip:{}, source port:{}, dest ip:{}, dest port:{}, proto:{}, size: {}",
aya_log::Ipv6Formatter::format(data.source_ipv6),
data.source_port,
aya_log::Ipv6Formatter::format(data.dest_ipv6),
data.dest_port,
utils::ip::print_proto(data.proto),
data.len);
con.addv6(
data.source_ipv6,
data.dest_ipv6,
data.source_port,
data.dest_port,
data.proto,
data.len,
)
.await;
}
_ => println!("result not coverd"),
}
}
}
});
}
info!("Waiting for Ctrl-C...");
signal::ctrl_c().await.unwrap();
info!("Exiting...");
Ok(())
}

154
net-logger/src/utils/ip.rs Normal file
View File

@@ -0,0 +1,154 @@
use network_types::ip::IpProto;
pub fn print_proto(proto: IpProto) -> String {
String::from(match proto {
IpProto::HopOpt => "HopOpt",
IpProto::Icmp => "Icmp",
IpProto::Igmp => "Igmp",
IpProto::Ggp => "Ggp",
IpProto::Ipv4 => "Ipv4",
IpProto::Stream => "Stream",
IpProto::Tcp => "Tcp",
IpProto::Cbt => "Cbt",
IpProto::Egp => "Egp",
IpProto::Igp => "Igp",
IpProto::BbnRccMon => "BbnRccMon",
IpProto::NvpII => "NvpII",
IpProto::Pup => "Pup",
IpProto::Argus => "Argus",
IpProto::Emcon => "Emcon",
IpProto::Xnet => "Xnet",
IpProto::Chaos => "Chaos",
IpProto::Udp => "Udp",
IpProto::Mux => "Mux",
IpProto::DcnMeas => "DcnMeas",
IpProto::Hmp => "Hmp",
IpProto::Prm => "Prm",
IpProto::Idp => "Idp",
IpProto::Trunk1 => "Trunk1",
IpProto::Trunk2 => "Trunk2",
IpProto::Leaf1 => "Leaf1",
IpProto::Leaf2 => "Leaf2",
IpProto::Rdp => "Rdp",
IpProto::Irtp => "Irtp",
IpProto::Tp4 => "Tp4",
IpProto::Netblt => "Netblt",
IpProto::MfeNsp => "MfeNsp",
IpProto::MeritInp => "MeritInp",
IpProto::Dccp => "Dccp",
IpProto::ThirdPartyConnect => "ThirdPartyConnect",
IpProto::Idpr => "Idpr",
IpProto::Xtp => "Xtp",
IpProto::Ddp => "Ddp",
IpProto::IdprCmtp => "IdprCmtp",
IpProto::TpPlusPlus => "TpPlusPlus",
IpProto::Il => "Il",
IpProto::Ipv6 => "Ipv6",
IpProto::Sdrp => "Sdrp",
IpProto::Ipv6Route => "Ipv6Route",
IpProto::Ipv6Frag => "Ipv6Frag",
IpProto::Idrp => "Idrp",
IpProto::Rsvp => "Rsvp",
IpProto::Gre => "Gre",
IpProto::Dsr => "Dsr",
IpProto::Bna => "Bna",
IpProto::Esp => "Esp",
IpProto::Ah => "Ah",
IpProto::Inlsp => "Inlsp",
IpProto::Swipe => "Swipe",
IpProto::Narp => "Narp",
IpProto::Mobile => "Mobile",
IpProto::Tlsp => "Tlsp",
IpProto::Skip => "Skip",
IpProto::Ipv6Icmp => "Ipv6Icmp",
IpProto::Ipv6NoNxt => "Ipv6NoNxt",
IpProto::Ipv6Opts => "Ipv6Opts",
IpProto::AnyHostInternal => "AnyHostInternal",
IpProto::Cftp => "Cftp",
IpProto::AnyLocalNetwork => "AnyLocalNetwork",
IpProto::SatExpak => "SatExpak",
IpProto::Kryptolan => "Kryptolan",
IpProto::Rvd => "Rvd",
IpProto::Ippc => "Ippc",
IpProto::AnyDistributedFileSystem => "AnyDistributedFileSystem",
IpProto::SatMon => "SatMon",
IpProto::Visa => "Visa",
IpProto::Ipcv => "Ipcv",
IpProto::Cpnx => "Cpnx",
IpProto::Cphb => "Cphb",
IpProto::Wsn => "Wsn",
IpProto::Pvp => "Pvp",
IpProto::BrSatMon => "BrSatMon",
IpProto::SunNd => "SunNd",
IpProto::WbMon => "WbMon",
IpProto::WbExpak => "WbExpak",
IpProto::IsoIp => "IsoIp",
IpProto::Vmtp => "Vmtp",
IpProto::SecureVmtp => "SecureVmtp",
IpProto::Vines => "Vines",
IpProto::Ttp => "Ttp",
IpProto::NsfnetIgp => "NsfnetIgp",
IpProto::Dgp => "Dgp",
IpProto::Tcf => "Tcf",
IpProto::Eigrp => "Eigrp",
IpProto::Ospfigp => "Ospfigp",
IpProto::SpriteRpc => "SpriteRpc",
IpProto::Larp => "Larp",
IpProto::Mtp => "Mtp",
IpProto::Ax25 => "Ax25",
IpProto::Ipip => "Ipip",
IpProto::Micp => "Micp",
IpProto::SccSp => "SccSp",
IpProto::Etherip => "Etherip",
IpProto::Encap => "Encap",
IpProto::AnyPrivateEncryptionScheme => "AnyPrivateEncryptionScheme",
IpProto::Gmtp => "Gmtp",
IpProto::Ifmp => "Ifmp",
IpProto::Pnni => "Pnni",
IpProto::Pim => "Pim",
IpProto::Aris => "Aris",
IpProto::Scps => "Scps",
IpProto::Qnx => "Qnx",
IpProto::ActiveNetworks => "ActiveNetworks",
IpProto::IpComp => "IpComp",
IpProto::Snp => "Snp",
IpProto::CompaqPeer => "CompaqPeer",
IpProto::IpxInIp => "IpxInIp",
IpProto::Vrrp => "Vrrp",
IpProto::Pgm => "Pgm",
IpProto::AnyZeroHopProtocol => "AnyZeroHopProtocol",
IpProto::L2tp => "L2tp",
IpProto::Ddx => "Ddx",
IpProto::Iatp => "Iatp",
IpProto::Stp => "Stp",
IpProto::Srp => "Srp",
IpProto::Uti => "Uti",
IpProto::Smp => "Smp",
IpProto::Sm => "Sm",
IpProto::Ptp => "Ptp",
IpProto::IsisOverIpv4 => "IsisOverIpv4",
IpProto::Fire => "Fire",
IpProto::Crtp => "Crtp",
IpProto::Crudp => "Crudp",
IpProto::Sscopmce => "Sscopmce",
IpProto::Iplt => "Iplt",
IpProto::Sps => "Sps",
IpProto::Pipe => "Pipe",
IpProto::Sctp => "Sctp",
IpProto::Fc => "Fc",
IpProto::RsvpE2eIgnore => "RsvpE2eIgnore",
IpProto::MobilityHeader => "MobilityHeader",
IpProto::UdpLite => "UdpLite",
IpProto::Mpls => "Mpls",
IpProto::Manet => "Manet",
IpProto::Hip => "Hip",
IpProto::Shim6 => "Shim6",
IpProto::Wesp => "Wesp",
IpProto::Rohc => "Rohc",
IpProto::EthernetInIpv4 => "EthernetInIpv4",
IpProto::Aggfrag => "Aggfrag",
IpProto::Test1 => "Test1",
IpProto::Test2 => "Test2",
IpProto::Reserved => "Reserved",
})
}

View File

@@ -0,0 +1 @@
pub mod ip;