Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add raw sockets #18

Closed
wants to merge 4 commits into from
Closed

Add raw sockets #18

wants to merge 4 commits into from

Conversation

batonius
Copy link
Contributor

Problem: smoltcp doesn't support raw sockets, low-level utilities like ping and custom IP protocols are impossible to implement.

Solution: implement raw sockets, provide an example of their usage.

Changes introduced by this pull request:

  • socket::udp::SocketBuffer has been factored out as generic common::RingBuffer.
  • socket::raw module has been implemented.
  • iface::EthernetInterface has been changed to support raw sockets, giving them priority over the built-in handlers.
  • The ping example has been implemented.

Status: the ping example works as expected.

TODOs:

  • Currently, smoltcp uses iteration over the list of sockets for packet dispatch, which is highly inefficient on the platforms where standard containers like maps and hash tables are available. The existing SocketSet container should be complimented with lookup tables for raw and tcp/udp sockets.

Other:

  • I'm using the current version of rustfmt for my code, sometimes it looks weird.
  • I've used some recent stable Rust features like field shorthands and pub(crate), so I've bumped Rust version in Readme.md to 1.18.

@batonius
Copy link
Contributor Author

Looks like the recent nightly broke the collections feature, master fails to build with it as well.

@whitequark
Copy link
Contributor

Looks like the recent nightly broke the collections feature, master fails to build with it as well.

This... is actually really troublesome.

@whitequark
Copy link
Contributor

I've used some recent stable Rust features like field shorthands and pub(crate), so I've bumped Rust version in Readme.md to 1.18.

This is 100% OK, I was going to start using those myself.

I'm using the current version of rustfmt for my code, sometimes it looks weird.

I dislike rustfmt and hand-format all of my code; so please don't mind if I ask to fix some particularly egregious cases.

@batonius
Copy link
Contributor Author

Done.

@@ -0,0 +1 @@
pub mod ring_buffer;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer this to be a pub mod smoltcp::storage, with associated documentation.

@@ -0,0 +1,118 @@
use managed::Managed;

pub trait Resetable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resettable, should be in smoltcp::storage, and needs documentation to explain the rationale.

pub struct RingBuffer<'a, T: 'a> {
storage: Managed<'a, [T]>,
read_at: usize,
length: usize,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would really like it if you at least kept the formatting as-is when moving code around. My preferences notwithstanding this makes git's copy/move tracking mechanisms work better.

for raw_socket in sockets.iter_mut().filter_map(Socket::as_raw) {
if let Ok(()) = raw_socket.process(timestamp, &IpRepr::Ipv4(ipv4_repr),
ipv4_packet.payload()) {
return Ok(());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not how raw sockets work on hosted systems. To quote Linux man pages:

When a packet is received, it is passed to any raw sockets which have been bound to its protocol before it is passed to other protocol handlers (e.g., kernel protocol modules).

Note that it does not say that the packet is not passed to other protocol handlers. Indeed, that's why TCP sockets still work even if you're running tcpdump (or ping) concurrently.

/// transmit and receive packet buffers.
#[derive(Debug)]
pub struct RawSocket<'a, 'b: 'a> {
ip_addr: IpAddress,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A raw socket isn't bound to an IP address, only to a protocol.

/// Enqueue a packet to be sent to the given IP address, and fill it from a slice.
///
/// See also [send](#method.send).
pub fn send_slice(&mut self, data: &[u8], ip_addr: IpAddress) -> Result<usize, ()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to accept IpAddress in send, or provide it in recv; it is the responsibility of the client to dissect the packet.

let startup_time = Instant::now();

loop {
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scope isn't necessary since ping only needs one socket.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's necessary because both sockets.get_mut and iface.poll take a mutable reference to sockets. It's the same as in the client.rs example.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, of course.

examples/ping.rs Outdated
"Can't allocate packet",
);
let mut packet =
Icmpv4Packet::new(raw_payload).expect("Can't create ICMPv4 packet");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just unwrap(). If we're going to add more proper error handling to examples it should be done in sync (and use error!, not expect()).

Icmpv4Repr::parse(&packet)
{
let seq_no = seq_no as usize;
if let Some(_) = waiting_queue.get(&seq_no) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can drastically simplify this by embedding the timestamp in the sent packet :) As a bonus the ping program will be possible to run with no_std code after this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already do embed the timestamp, waiting_queue is used for timeout calculation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed that, sorry. Hm... should be fine the way it is, then.

@batonius
Copy link
Contributor Author

I'd like to keep IpAddress in RawSocket for two reasons: first, since there's no routing yet, we can't determine source IP from destination IP alone, so we need a default source IP. Second, it works as an IPv4/IPv6 marker.

@whitequark
Copy link
Contributor

first, since there's no routing yet, we can't determine source IP from destination IP alone

Take a closer look at EthernetInterface. It already selects a source IP as "first IP that matches destination IP protocol" in a few code paths.

it works as an IPv4/IPv6 marker

Why is a marker necessary? The first nibble of the packet has that already.

@batonius
Copy link
Contributor Author

batonius commented Jun 17, 2017

It already selects a source IP as "first IP that matches destination IP protocol" in a few code paths.

Right, I see it now.

Why is a marker necessary?

A raw socket is bound to a version of IP on creation, by using either AF_INET or AF_INET6 as the socket_family argument of socket, this means an IPv4 raw socket should ignore IPv6 packets, and vice versa.

@whitequark
Copy link
Contributor

A raw socket is bound to a version of IP on creation, by using either AF_INET or AF_INET6 as the socket_family argument of socket, this means an IPv4 raw socket should ignore IPv6 packets, and vice versa.

Let's add a smoltcp::wire::IpVersion enum for this and maybe other cases.

@whitequark
Copy link
Contributor

Let's add a smoltcp::wire::IpVersion enum for this and maybe other cases.

Actually, hang on. It might be not so wise to tie it to IP. Raw sockets that receive, for example, Ethernet frames, 802.11 frames, 802.15.4 frames, are all still useful. Let's make a enum specifically listing what a raw socket can receive, instead.

README.md Outdated
@@ -214,6 +214,23 @@ cargo run --example client -- tap0 ADDRESS PORT
It connects to the given address (not a hostname) and port (e.g. `socat stdio tcp4-listen 1234`),
and will respond with reversed chunks of the input indefinitely.

### examples/ping.rs

_examples/ping.rs_ implements a minimail version of the `ping` utility using raw sockets.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal
(just passing by)

@batonius
Copy link
Contributor Author

batonius commented Jun 17, 2017

Wait. IpRepr::lower only replaces src_ip for IpRepr::Unspecified, and I'm calling emit in RawSocket::dispatch with IpRepr::Ipv4 I've just parsed from the packet I've gotten from the tx buffer. So EthernetInterface doesn't replace src_ip in this case.

@whitequark
Copy link
Contributor

Wait. IpRepr::lower only replaces src_ip for IpRepr::Unspecified, and I'm calling emit in RawSocket::dispatch with IpRepr::Ipv4 I've just parsed from the packet I've gotten from the tx buffer. So EthernetInterface doesn't replace src_ip in this case.

Of course, IP addresses also have unspecified ranges, so we can make EthernetInterface look at that too.

@batonius
Copy link
Contributor Author

So, I make an Unspecified repr from Ipv4Repr, turning all-zero src_addr into an Unspecified addr. It seems to work with all-zero src_addr in ping.

@whitequark
Copy link
Contributor

So, I make an Unspecified repr from Ipv4Repr, turning all-zero src_addr into an Unspecified addr. It seems to work with all-zero src_addr in ping.

I think it's a better idea to change the condition in EthernetInterface to check for unspecified source address in any kind of IpRepr (via a query method on the latter); this will make any kind of sockets, such as ICMP sockets, work the same.

@batonius
Copy link
Contributor Author

Why is Ipv4Addr::UNSPECIFIED commented out? It looks like a useful constant to have.

@whitequark
Copy link
Contributor

Why is Ipv4Addr::UNSPECIFIED commented out? It looks like a useful constant to have.

I'd like to have it as an associated constant but those aren't in stable yet.

@whitequark
Copy link
Contributor

They'll be stable soon, rust-lang/rust#29646.

@batonius
Copy link
Contributor Author

I'm not sure if I should remove the address replacement logic from IpRepr::lower, it doesn't look like the right place for it, but it's being called from tcp.rs and I have no desire to touch it yet :)

@whitequark
Copy link
Contributor

Your specify_src_addr should be just a part of lower.

@batonius
Copy link
Contributor Author

Done. I'm just not sure if the src address replacement should be a part of lowering IpRepr to concrete representation, it looks like two separate actions to me. But if specify_src_addr isn't needed anywhere else, then it might make sense.

@whitequark
Copy link
Contributor

whitequark commented Jun 17, 2017

I'm just not sure if the src address replacement should be a part of lowering IpRepr to concrete representation

Address replacement is the whole point of IpRepr::lower, the exact representation details are irrelevant. The name might not have been the best choice.

And I'm not happy with your fix. There shouldn't be a new_unspecified, we'll have the associated const once 1.20 is released. And splitting lower into two functions, one of which is a mutator and the other is not, is not helping with clarity here.

@whitequark
Copy link
Contributor

Thanks! Any chance you could update (or add?) a test for IpRepr::lower?

@@ -179,6 +179,11 @@ impl<'a, 'b, 'c, DeviceT: Device + 'a> Interface<'a, 'b, 'c, DeviceT> {
&eth_frame.src_addr());
}

for raw_socket in sockets.iter_mut().filter_map(Socket::as_raw) {
let _ = raw_socket.process(timestamp, &IpRepr::Ipv4(ipv4_repr),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried about this let _ =. What errors can happen here besides Exhausted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error::Rejected, and it definitely should be ignored.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, so only that. Then can you add a match that ignores this error and panics on any other? That will be robust in light of code evolution (or help when fuzzing).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean return Err(e), not panic? Becuase you do return Err(e) for socket errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panics, through unreachable!(), since those branches should be unreachable indeed.

Socket::Raw(ref mut raw) => Some(raw),
_ => None,
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think single-use helper functions are necessary. Just use as_socket where this would be called...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is a different issue, as_socket doesn't let us interrogate. Sure. Let's add try_as_socket in addition to as_socket to the AsSocket trait, that would be much cleaner.

#[derive(Debug)]
pub struct RawSocket<'a, 'b: 'a> {
payload_type: PayloadType,
ip_proto: IpProtocol,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to have separate payload_type and ip_proto fields because the latter is strictly derived from the former.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? payload_type could be Ipv4 or Ipv6 and ip_proto is independent of that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, of course.

Okay, just rename it to ip_protocol.

PayloadType::Ipv4 => {
let mut ipv4_packet = Ipv4Packet::new(packet_buf.as_mut())?;
ipv4_packet.fill_checksum();
let ipv4_packet = Ipv4Packet::new(&*ipv4_packet.into_inner())?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the reinitialization?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ipv4Repr::parse requires non-mutable Ipv4Packet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&mut T to &T is a valid cast, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That what I thought, but it doesn't work for me for some reason:

error[E0308]: mismatched types
   --> socket/raw.rs:185:49
    |
185 |                 let ipv4_repr = Ipv4Repr::parse(&ipv4_packet)?;
    |                                                 ^^^^^^^^^^^^ types differ in mutability
    |
    = note: expected type `&wire::ipv4::Packet<&_>`
               found type `&wire::ipv4::Packet<&mut [u8]>`
    = help: here are some functions which might fulfill your needs:
            - .into_inner()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh right, that's an artifact of how my wire impls are done, nevermind.

///
/// In-palace analog of Default.
/// Used by RingBuffer for initializing a storage.
pub trait Resetable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resettable, and let's move it to smoltcp::storage since it is not really specific to ring buffers.


/// A trait for setting a value to the default state.
///
/// In-palace analog of Default.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In-place

@whitequark
Copy link
Contributor

Okay, this is much better. After the last round of review I don't think there will be any further problems.

@batonius
Copy link
Contributor Author

I believe that's all with the change requests.

@whitequark
Copy link
Contributor

Wonderful, thanks! Do you think you can rebase the commits?

@batonius
Copy link
Contributor Author

Something like this?

@whitequark
Copy link
Contributor

Yup! I'll merge tomorrow, I think.

@batonius
Copy link
Contributor Author

Good. I have some ideas about packet dispatching but I think this time I'll open an issue to discuss it first.

@whitequark
Copy link
Contributor

Sure, go ahead!

@batonius batonius mentioned this pull request Jun 18, 2017
@whitequark
Copy link
Contributor

Merged, with stylistic, documentation, and formatting changes. No functional changes.

Most notably I've replaced socket::RawPayloadType with a wire::IpVersion since our raw sockets are specifically IP raw sockets.

@whitequark whitequark closed this Jun 21, 2017
@batonius batonius deleted the raw_sockets branch June 21, 2017 08:47
@whitequark
Copy link
Contributor

@batonius Incidentally--did you decide to do anything about ICMP sockets? I think (out of stuff that redox supports right now) ICMP sockets and DHCP support are definitely in scope of smoltcp, and DNS support almost certainly is. At the very least the wire::dhcp and wire::dns modules would be useful; DNS is stateless so there is perhaps not much more to be done, and a DHCP state machine should certainly live inside smoltcp, not outside.

@batonius
Copy link
Contributor Author

My current goal is to replace the two low-level network daemons, ethernetd and ipd, with a unified one based on smoltcp, while keeping the interface ipd provides to tcpd, udpd and icmpd, which is basically raw sockets. Eventually, I'd like to have ICMP sockets, DHCP and DNS all integrated into that unified daemon, but only after I integrate udpd and tcpd into it.

@whitequark
Copy link
Contributor

Sounds good to me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants