From 3ac3c8ed4bea43e141db9d77bbd56a3b4a4f304e Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 12 Jan 2021 21:10:11 -0500 Subject: [PATCH] Roll our own ICMPv6 Packet parser --- Cargo.toml | 5 +- src/echo.rs | 74 ++++++++++++ src/lib.rs | 5 +- src/packet.rs | 329 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/socket.rs | 94 +++------------ 5 files changed, 428 insertions(+), 79 deletions(-) create mode 100644 src/echo.rs create mode 100644 src/packet.rs diff --git a/Cargo.toml b/Cargo.toml index a41e364..27a9a9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "icmp" +name = "icmp-socket" version = "0.1.0" authors = ["Jeremy Wall "] edition = "2018" @@ -8,4 +8,5 @@ edition = "2018" [dependencies] socket2 = "0.3.19" -packet = "0.1.4" \ No newline at end of file +packet = "0.1.4" +byteorder = "1.3.4" \ No newline at end of file diff --git a/src/echo.rs b/src/echo.rs new file mode 100644 index 0000000..b65d2e3 --- /dev/null +++ b/src/echo.rs @@ -0,0 +1,74 @@ +// Copyright 2021 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use std::convert::From; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use packet::{Builder, Packet as P}; +use packet::icmp::echo::Packet; + +// TODO(jwall): It turns out that the ICMPv6 packets are sufficiently +// different from the ICMPv4 packets. In order to handle them appropriately +// It is going to take some consideration. +use crate::{IcmpSocket4, IcmpSocket6}; + +pub struct EchoResponse { + identifier: u16, + sequence: u16, + payload: Vec, +} + +pub struct EchoSocket4 { + sequence: u16, + buf: Vec, + inner: IcmpSocket4, +} + +impl EchoSocket4 { + pub fn new(sock: IcmpSocket4) -> Self { + EchoSocket4{inner:sock, sequence: 0, buf: Vec::with_capacity(512)} + } + + pub fn set_max_hops(&mut self, hops: u32) { + self.inner.set_max_hops(hops); + } + + pub fn send_ping(&mut self, dest: Ipv4Addr, identifier: u16, payload: &[u8]) -> std::io::Result<()> { + let packet = packet::icmp::Builder::default() + .echo().unwrap().request().unwrap() + .identifier(identifier).unwrap() + .sequence(self.sequence).unwrap() + .payload(payload).unwrap().build().unwrap(); + self.sequence += 1; + self.inner.send_to(dest, &packet)?; + Ok(()) + } + + pub fn recv_ping(&mut self) -> std::io::Result { + let bytes_read = self.inner.rcv_from(&mut self.buf)?; + match Packet::new(&self.buf[0..bytes_read]) { + Ok(p) => return Ok(EchoResponse{ + sequence: p.sequence(), + identifier: p.identifier(), + payload: p.payload().to_owned(), + }), + Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Malformed ICMP Response: {:?}", e))), + }; + } +} + +impl From for EchoSocket4 { + fn from(sock: IcmpSocket4) -> Self { + EchoSocket4::new(sock) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index a1b2c01..fb224bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,5 +13,8 @@ // limitations under the License. pub mod socket; +pub mod echo; +pub mod packet; -pub use socket::{IcmpSocket, IcmpSocket4, IcmpSocket6}; \ No newline at end of file +pub use crate::packet::{Icmpv6Packet, Icmpv6Message}; +pub use socket::{IcmpSocket4, IcmpSocket6}; \ No newline at end of file diff --git a/src/packet.rs b/src/packet.rs new file mode 100644 index 0000000..08cbfb2 --- /dev/null +++ b/src/packet.rs @@ -0,0 +1,329 @@ +// Copyright 2021 Jeremy Wall +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use std::net::Ipv6Addr; +use byteorder::{ByteOrder, BigEndian}; + +fn ipv6_sum_words(ip: &Ipv6Addr) -> u32 { + ip.segments().iter().map(|x| *x as u32).sum() +} + +fn sum_big_endian_words(bs: &[u8]) -> u32 { + if bs.len() == 0 { + return 0; + } + + let len = bs.len(); + let mut data = &bs[..]; + let mut sum = 032; + // We need to stop when we have less than 2 bytes left. + while data.len() >= 2 { + sum += BigEndian::read_u16(&data[0..2]) as u32; + // remove the first two now that we've already summed them + data = &data[2..]; + } + + if len % 2 != 0 { // If odd then checksum the last byte + sum += (bs[len - 1] as u32) << 8; + } + return sum; +} + +pub enum Icmpv6Message { + // NOTE(JWALL): All of the below integers should be parsed as big endian on the + // wire. + Unreachable { + _unused: u32, + invoking_packet: Vec, + }, + PacketTooBig { + mtu: u32, + invoking_packet: Vec, + }, + TimeExceeded { + _unused: u32, + invoking_packet: Vec, + }, + ParameterProblem { + pointer: u32, + invoking_packet: Vec, + }, + PrivateExperimental { + padding: u32, + payload: Vec, + }, + EchoRequest { + identifier: u16, + sequence: u16, + payload: Vec, + }, + EchoReply { + identifier: u16, + sequence: u16, + payload: Vec, + } +} + +use Icmpv6Message::{Unreachable, PacketTooBig, TimeExceeded, ParameterProblem, PrivateExperimental, EchoRequest, EchoReply}; + +impl Icmpv6Message { + pub fn get_bytes(&self) -> Vec { + let mut bytes = Vec::new(); + match self { + Unreachable {_unused: field1, invoking_packet: field2 } | + PacketTooBig {mtu: field1, invoking_packet: field2 } | + TimeExceeded {_unused: field1, invoking_packet: field2 } | + ParameterProblem {pointer: field1, invoking_packet: field2 } | + PrivateExperimental {padding: field1, payload: field2 } => { + let mut buf = vec![0; 4]; + BigEndian::write_u32(&mut buf, *field1); + bytes.append(&mut buf); + bytes.extend_from_slice(field2); + }, + EchoRequest{ + identifier, + sequence, + payload, + } | EchoReply{ + identifier, + sequence, + payload, + } => { + let mut buf = vec![0; 2]; + BigEndian::write_u16(&mut buf, *identifier); + bytes.append(&mut buf); + buf.resize(2, 0); + BigEndian::write_u16(&mut buf, *sequence); + bytes.append(&mut buf); + bytes.extend_from_slice(payload); + } + } + bytes + } +} + +pub struct Icmpv6Packet { + // NOTE(JWALL): All of the below integers should be parsed as big endian on the + // wire. + pub typ: u8, + pub code: u8, + pub checksum: u16, + pub message: Icmpv6Message, +} + +#[derive(Debug)] +pub enum PacketParseError { + PacketTooSmall(usize), + UnrecognizedICMPType, +} + +impl Icmpv6Packet { + /// Construct a packet by parsing the provided bytes. + pub fn parse>(bytes: B) -> Result { + let bytes = bytes.as_ref(); + // NOTE(jwall): All ICMP packets are at least 8 bytes long. + if bytes.len() < 8 { + return Err(PacketParseError::PacketTooSmall(bytes.len())); + } + let (typ, code, checksum) = (bytes[0], bytes[1], BigEndian::read_u16(&bytes[2..3])); + let next_field = BigEndian::read_u32(&bytes[4..7]); + let payload = bytes[8..].to_owned(); + let message = match typ { + 1 => Unreachable{ + _unused: next_field, + invoking_packet: payload, + }, + 2 => PacketTooBig{ + mtu: next_field, + invoking_packet: payload, + }, + 3 => TimeExceeded{ + _unused: next_field, + invoking_packet: payload, + }, + 4 => ParameterProblem { + pointer: next_field, + invoking_packet: payload, + }, + 100 | 101 | 200 | 201 => PrivateExperimental{ + padding: next_field, + payload: payload, + }, + 128 => EchoRequest{ + identifier: BigEndian::read_u16(&bytes[4..5]), + sequence: BigEndian::read_u16(&bytes[6..7]), + payload: payload, + }, + 129 => EchoReply{ + identifier: BigEndian::read_u16(&bytes[4..5]), + sequence: BigEndian::read_u16(&bytes[6..7]), + payload: payload, + }, + _ => return Err(PacketParseError::UnrecognizedICMPType), + }; + return Ok(Icmpv6Packet{ + typ: typ, + code: code, + checksum: checksum, + message: message, + }) + } + + /// Get this packet serialized to bytes suitable for sending on the wire. + pub fn get_bytes(&self, with_checksum: bool) -> Vec { + let mut bytes = Vec::new(); + bytes.push(self.typ); + bytes.push(self.code); + let mut buf = Vec::with_capacity(2); + buf.resize(2, 0); + BigEndian::write_u16(&mut buf, if with_checksum { + self.checksum + } else { + 0 + }); + bytes.append(&mut buf); + bytes.append(&mut self.message.get_bytes()); + return bytes; + } + + /// Calculate the checksum for the packet given the provided source and destination + /// addresses. + pub fn calculate_checksum(&self, source: &Ipv6Addr, dest: &Ipv6Addr) -> u16 { + // First sum the pseudo header + let mut sum = 0u32; + sum += ipv6_sum_words(source); + sum += ipv6_sum_words(dest); + // according to rfc4443: https://tools.ietf.org/html/rfc4443#section-2.3 + // the ip next header value is 58 + sum += 58u32; + + let bytes = self.get_bytes(false); + let len = bytes.len(); + sum += len as u32; + // Then append the message bytes as a byte buffer starting with the message + // type field with the checksum field set to 0. + sum += sum_big_endian_words(&bytes); + + // handle the carry + while sum >> 16 != 0 { + sum = (sum >> 16) + (sum & 0xFFFF); + } + !sum as u16 + } + + /// Fill the checksum for the packet using the given source and destination + /// addresses. + pub fn with_checksum(mut self, source: &Ipv6Addr, dest: &Ipv6Addr) -> Self { + self.checksum = self.calculate_checksum(source, dest); + self + } + + /// Construct a packet for Destination Unreachable messages. + pub fn with_unreachable(code: u8, packet: Vec) -> Result { + if code > 6 { + return Err(Icmpv6PacketBuildError::InvalidCode(code)); + } + Ok(Self { + typ: 1, + code: code, + checksum: 0, + // TODO(jwall): Should we enforce that the packet isn't too big? + // It is not supposed to be larger than the minimum IPv6 MTU + message: Unreachable{ + _unused: 0, + invoking_packet: packet, + }, + }) + } + + /// Construct a packet for Packet Too Big messages. + pub fn with_packet_too_big(mtu: u32, packet: Vec) -> Result { + Ok(Self{ + typ: 2, + code: 0, + checksum: 0, + // TODO(jwall): Should we enforce that the packet isn't too big? + // It is not supposed to be larger than the minimum IPv6 MTU + message: PacketTooBig{ + mtu: mtu, + invoking_packet: packet, + }, + }) + } + + /// Construct a packet for Time Exceeded messages. + pub fn with_time_exceeded(code: u8, packet: Vec) -> Result { + if code > 1 { + return Err(Icmpv6PacketBuildError::InvalidCode(code)); + } + Ok(Self{ + typ: 3, + code: code, + checksum: 0, + // TODO(jwall): Should we enforce that the packet isn't too big? + // It is not supposed to be larger than the minimum IPv6 MTU + message: TimeExceeded{ + _unused: 0, + invoking_packet: packet, + }, + }) + } + + /// Construct a packet for Parameter Problem messages. + pub fn with_parameter_problem(code: u8, pointer: u32, packet: Vec) -> Result { + if code > 1 { + return Err(Icmpv6PacketBuildError::InvalidCode(code)); + } + Ok(Self { + typ: 4, + code: code, + checksum: 0, + message: ParameterProblem{ + pointer: pointer, + invoking_packet: packet, + } + }) + } + + /// Construct a packet for Echo Request messages. + pub fn with_echo_request(identifier: u16, sequence: u16, payload: Vec) -> Result { + Ok(Self { + typ: 4, + code: 0, + checksum: 0, + message: EchoRequest{ + identifier: identifier, + sequence: sequence, + payload: payload, + } + }) + } + + /// Construct a packet for Echo Reply messages. + pub fn with_echo_reply(identifier: u16, sequence: u16, payload: Vec) -> Result { + Ok(Self { + typ: 4, + code: 0, + checksum: 0, + message: EchoReply{ + identifier: identifier, + sequence: sequence, + payload: payload, + } + }) + } +} + +pub enum Icmpv6PacketBuildError { + InvalidCode(u8), +} \ No newline at end of file diff --git a/src/socket.rs b/src/socket.rs index ce452b6..70bea9b 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::convert::{TryFrom, Into, TryInto}; +use std::convert::{TryFrom, Into}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use socket2::{Socket, Domain, Type, Protocol}; +use crate::packet::Icmpv6Packet; + fn ip_to_socket(ip: &IpAddr) -> SocketAddr { format!("{}:0", ip).parse::().unwrap() } @@ -52,10 +54,8 @@ impl IcmpSocket4 { Ok(()) } + // TODO(jwall): This should take an actual packet not the payload. pub fn send_to(&mut self, dest: Ipv4Addr, payload: &[u8]) -> std::io::Result<()> { - //let packet = packet::icmp::Builder::default() - // .echo().unwrap().request().unwrap().identifier(42).unwrap().sequence(seq) - // .unwrap().payload(payload).unwrap().build().unwrap(); let dest = ip_to_socket(&IpAddr::V4(dest)); self.inner.set_ttl(self.opts.hops)?; self.inner.send_to(payload, &(dest.into()))?; @@ -95,100 +95,42 @@ impl IcmpSocket6 { Ok(()) } - pub fn send_to(&mut self, dest: Ipv6Addr, payload: &[u8]) -> std::io::Result<()> { - //let packet = packet::icmp::Builder::default() - // .echo().unwrap().request().unwrap().identifier(42).unwrap().sequence(seq) - // .unwrap().payload(payload).unwrap().build().unwrap(); + // TODO(jwall): This should take an actual packet not the payload. + pub fn send_to(&mut self, dest: Ipv6Addr, mut packet: Icmpv6Packet) -> std::io::Result<()> { + let source = match self.bound_to { + Some(ref addr) => addr, + None => return Err(std::io::Error::new(std::io::ErrorKind::Other, "Socket not bound to an address")), + }; + packet = packet.with_checksum(source, &dest); let dest = ip_to_socket(&IpAddr::V6(dest)); self.inner.set_unicast_hops_v6(self.opts.hops)?; - self.inner.send_to(&payload, &(dest.into()))?; + self.inner.send_to(&packet.get_bytes(true), &(dest.into()))?; Ok(()) } - - + + // TODO(jwall): This should return a packet not bytes. pub fn rcv_from(&self, buf: &mut [u8]) -> std::io::Result { let (read_count, _addr) = self.inner.recv_from(buf)?; Ok(read_count) } } -pub enum IcmpSocket { - V4(IcmpSocket4), - V6(IcmpSocket6), -} - -use IcmpSocket::{V4, V6}; - -impl IcmpSocket { - pub fn new_v4() -> std::io::Result { - Ok(V4(IcmpSocket4::new()?)) - } - - pub fn new_v6() -> std::io::Result { - Ok(V6(IcmpSocket6::new()?)) - } - - pub fn with_ip(ip: IpAddr) -> std::io::Result { - ip.try_into() - } - - pub fn set_max_hops(&mut self, hops: u32) { - match self { - V4(ref mut s) => s.set_max_hops(hops), - V6(ref mut s) => s.set_max_hops(hops), - } - } - pub fn send_to(&mut self, dest: IpAddr, payload: &[u8]) -> std::io::Result<()> { - match dest { - IpAddr::V4(ip) => if let V4(ref mut sock) = self { - sock.send_to(ip, payload)?; - } else { - return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Attempt to send to IPv4 dest {} from IPv6 source", ip))); - }, - IpAddr::V6(ip) => if let V6(ref mut sock) = self { - sock.send_to(ip, payload)?; - } else { - return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Attempt to send to IPv6 dest {} from IPv4 source", ip))); - }, - } - Ok(()) - } - - pub fn rcv_from(&self, buf: &mut [u8]) -> std::io::Result { - match self { - V4(ref s) => s.rcv_from(buf), - V6(ref s) => s.rcv_from(buf), - } - } -} - -impl TryFrom for IcmpSocket { - type Error = std::io::Error; - - fn try_from(addr: IpAddr) -> Result { - match addr { - IpAddr::V4(addr) => addr.try_into(), - IpAddr::V6(addr) => addr.try_into(), - } - } -} - -impl TryFrom for IcmpSocket { +impl TryFrom for IcmpSocket4 { type Error = std::io::Error; fn try_from(addr: Ipv4Addr) -> Result { let mut sock = IcmpSocket4::new()?; sock.bind(addr)?; - Ok(IcmpSocket::V4(sock)) + Ok(sock) } } -impl TryFrom for IcmpSocket { +impl TryFrom for IcmpSocket6 { type Error = std::io::Error; fn try_from(addr: Ipv6Addr) -> Result { let mut sock = IcmpSocket6::new()?; sock.bind(addr)?; - Ok(IcmpSocket::V6(sock)) + Ok(sock) } }