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 support for IP mediums, v1 #336

Closed
wants to merge 4 commits into from

Conversation

Dirbaio
Copy link
Member

@Dirbaio Dirbaio commented Apr 17, 2020

Opening this PR so I can get feedback earlier in case there are problems with the approach I'm taking.

This is a first draft of #334. The goal is to split EthernetInterface into Ethernet and IP parts, so the IP parts can be reused in other non-ethernet interface types (linux tun, VPNs, PPP).

The IP module defines the following structs:

  • Config: contains IP configuration (addresses, routes) that doesn't change when processing IP packets.
  • State: contains state that can change when processing IP packets.
  • Processor: contains a &Config and a &mut State. Most of the functions moved from ethernet are here. This is just a convenience wrapper, as most functions need the config and the state, so they don't have to be passed around as parameters all the time.

The motivation for separating Config and State is that all the ethernet code needs read-only access to Config. This way Config can be immutably borrowed many times and shared everywhere it's needed, at the same time State is mutably borrowed to process packets. Without this I was getting borrow issues with both the ethernet and ip code needing to access the ip config.

For packet egress, the Ethernet layer passes a struct implementing a Dispatcher traint to the IP layer, which is used to pass IP packets back to the Ethernet layer.

Missing items:

  • Tests. I'll update them as soon as we've confirmed the code structure is OK
  • IPv6 NDISC. I don't know what to do, I have 2 ideas:
    • Have ethernet::Interface inject a Socket that handles NDISC. The socket would have to mut borrow ethernet::InnerInterface so it would have to be created on the fly every time, not sure if it's even possible.
    • Pass a process_ndisc callback to process_ipv6... ugly but should work?
  • Some random FIXMEs in the code.

My questions:

  1. any general feedback on the taken approach, or possible improvements?
  2. Most of the functions in the Ethernet code take as param the ip::Config, passing it around is somewhat annoying. This could benefit from an abstraction similar to ip::Processor: ethernet::InterfaceInner would become ethernet::State, and there would be an ethernet::Processor with ip_config, ethernet_state. Pros: no passing around ip all the time, design is consistent with IP. Cons: it's an extra struct. What do you think?
  3. Should the IP structs be pub or pub(crate)? I guess the latter, unless we want people to implement their interfaces out-of-tree?
  4. Is the route table an ethernet concept or an IP concept? To me it looks like it is an Ethernet concept. The only use in the IP code is for AnyIP. To me it feels a bit strange that AnyIP reads the route table instead of just using the CIDRs in ip_addrs. (Also AFAICT these CIDRs aren't used for anything?). If we change it to use just ip_addrs, then routes could be moved to ethernet.

@Dirbaio Dirbaio marked this pull request as ready for review April 17, 2020 01:59
@Dirbaio Dirbaio marked this pull request as draft April 17, 2020 02:00
@therealprof
Copy link
Contributor

I had a quick glance and I do like the idea.

Is the route table an ethernet concept or an IP concept?

Routing is layer 3 together with IP, Ethernet is layer 2 in the ISO/OSI model.

@whitequark
Copy link
Contributor

The ISO/OSI model isn't terribly relevant to real-world TCP/IP stacks; TCP is not separate from IP, and IP is not easily separate from Ethernet. Rather than considering this in abstract terms, it is best to see what effect any particular kind of separation has on your network stack, and go from that.

@Dirbaio I think your idea about separating Config and State is quite interesting. Do you think we could apply it more incrementally, i.e. first splitting the Ethernet structs into two, and only then splitting EthernetInterface?

@therealprof
Copy link
Contributor

IP is not easily separate from Ethernet.

I disagree. There's absolutely no relationship between Ethernet and IP whatsoever. You can run a lot of different protocols on top of Ethernet and there're also some commonly used IP implementations which do not run on top of Ethernet, e.g. PPP.

In this particular case the question was about routing and since routing can only work on top of a protocol like IP, there's no question for me that this does not belong in Ethernet at all which is why I brought up the ISO/OSI layers.

Rather than considering this in abstract terms, it is best to see what effect any particular kind of separation has on your network stack, and go from that.

I agree in priciple but in this particular case it seems pretty clear that routing does not belong into Ethernet.

@whitequark
Copy link
Contributor

whitequark commented Apr 18, 2020

You can run a lot of different protocols on top of Ethernet

Of course, I never implied that you can't.

there're also some commonly used IP implementations which do not run on top of Ethernet, e.g. PPP

Of course also true.

There's absolutely no relationship between Ethernet and IP whatsoever.

And this is just black-and-white thinking that obscures the real situation. If you said that IP is not strictly coupled to Ethernet in the way that TCP is coupled to IP, I would agree. However, a practical IPv4 stack, for example, cannot have a clean separation beteween the LLC sublayer and the IP layer because the concept of default gateways violates it. There is a similar issue with IPv6 NDISC. We also have DHCP, a layer 3 configuration protocol that runs on layer 4.

That is what I mean by "not easily separate": not in the sense that it is impossible to decouple IP from Ethernet (of course it is), but in the sense that real world IP implementations are a lot more messy than the model might lead you to think, and have interdepenencies between the Ethernet and IP parts, as this PR very clearly demonstrates.

The reason I bring it up is because one of the core principles of smoltcp, one that I decided on before I even wrote any code for the project, is to implement the protocols as they exist in real world and serve real users rather than adhere to a rigid model that doesn't fully apply to our system. That is something I would like to be preserved.


I agree in priciple but in this particular case it seems pretty clear that routing does not belong into Ethernet.

We are in agreement on this issue. I mostly wanted to provide historical context and explain the principles I used when designing smoltcp.

This is useful to allow the Config to be immutably borrowed from
many places at the same time State is mutably borrowed by the
packet processing code.

While not very useful on its own, this is one of many small refactors that
will make it possible to split the Ethernet code into Ethernet-specific
and IP-specific logic.
@Dirbaio Dirbaio force-pushed the ip-interface branch 2 times, most recently from 46d4cb0 to ecde918 Compare April 19, 2020 03:26
@Dirbaio
Copy link
Member Author

Dirbaio commented Apr 19, 2020

I've pushed a new version of the patches following @whitequark's suggestion. First commit splits ethernet into config+state, last commit splits these into a config+state for IP and another for Ethernet.

I find this way makes Ethernet and IP code more consistent, and hopefully makes review easier. Also, there's no more passing around ip::Config since the Ethernet processor now has access to it.

@whitequark
Copy link
Contributor

@Dirbaio I've reviewed them and I do very much like the first two commits! (Some tiny style nits, that's it.)

Regarding the third, do you think you can summarize the change in a few sentences or pargraphs? It's a massive diff and that would make it a lot easier for me to review.

Functions that only deal with IP packets return ip::Packet. ip::Packet's
are wrapped into ethernet::Packet's as late as possible.

This will later allow moving IP functions to the IP interface code when splitting
Ethernet and IP interfaces.
@Dirbaio
Copy link
Member Author

Dirbaio commented Apr 26, 2020

I've pushed a new set of commits, splitting the last commit in two. First one splits Packet, second one splits Config, State, Processor. I'm afraid I can't think of more ways to split the last one.

Also, commit messages now have more info, summarizing the changes.

- State, Config and Processor all get split in Ethernet and IP counterparts.
- Functions that only deal with IP packets are moved to the IP module, mostly
  as-is. No changes to the logic have been done, only changes to get the code to build.
- 2 new traits are introduced:
    - LowerDispatcher (for outgoing packets)
    - LowerProcessor (for incoming packets).
  The IP module uses them to call back into the Ethernet module, so it can handle
  Ethernet-specific concerns (arp, ndisc) and send packets to the Device.
- Ethernet's process_ipv{4,6} deal with the Ethernet-specific concerns and then call their IP counterparts.
@Dirbaio
Copy link
Member Author

Dirbaio commented Apr 26, 2020

Pushed new commits.

  • Implemented ndisc. (I had to add two traits, LowerDispatcher and LowerProcessor. I tried doing it with just one, but it runs into borrow issues)
  • Updated tests to the new structure (they all pass without needing logic changes).

With these, this is ready for review now.

@Dirbaio Dirbaio marked this pull request as ready for review April 26, 2020 21:06
@Dirbaio
Copy link
Member Author

Dirbaio commented Apr 26, 2020

Tests are failing due to an unrelated breakage from latest nightly. I've sent a separate PR for that: #337

@Dirbaio Dirbaio changed the title WIP: Separate IP parts from EthernetInterface. Separate IP parts from EthernetInterface. Apr 26, 2020
@whitequark
Copy link
Contributor

Thanks for splitting the changes. I like your general approach.

You've added a bunch of new pub (not pub(crate)) functions; is that intended?

Could you go through the added documentation and fix up the formatting and capitalization to be consistent with existing doc strings? This isn't terribly important but makes it nicer to read.

@lattice0
Copy link
Contributor

I'm trying to make it work by comparing with the ethernet implementation. I'd like to create an HTTP client example. As of now, ip.rs has no polling function poll. Shouldn't it have? So an example client would be almost like the one we already have on examples/client.rs. Is there a reason on why you didn't implement it, @Dirbaio?

@Dirbaio
Copy link
Member Author

Dirbaio commented May 20, 2020

IP is supposed to be used only internally from the actual interface structs, it's not supposed to be an interface itself. For example if you want to use a tun linux device you'd create a TunInterface that uses IP internally. poll & co methods would be duplicated, maybe there's a way around that

I'm planning to send a follow-up PR adding it.

@lattice0
Copy link
Contributor

@Dirbaio since I want to use this stack on OpenVPN, instead of creating a TunInterface impementing the Device trait that taks with a linux file descriptor, I'm thinking about creating a VirtualTunInterface that reads and writes packets from memory, so I can integrate into OpenVPN. The usage of a VirtualTunInterface would be the same as the usage of TunInterface, so I created a tun.rs file that mimics ethernet.rs. And then I created virtual_tun.rs that implements a virtual TUN device. Maybe you can implement the TunInterface later.

I believe this is what should be done. Can you take a look at my sketch? https://github.com/Dirbaio/smoltcp/compare/ip-interface...lucaszanella:ip-interface?expand=1

I'm willing to do the tun.rs for you, since the only difference would be the VirtualTunInterface that implements the Device trait which is minimal. But I wanted to know if that's what you have in mind.

Icmpv6Repr::EchoReply { .. } => Ok(None),

// Forward any NDISC packets to the ndisc packet handler
#[cfg(feature = "ethernet")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this really only needed for ethernet?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also wanna know this. Had problems with this function on my code changes

@lattice0
Copy link
Contributor

Hey @Dirbaio, if you look at https://github.com/Dirbaio/smoltcp/compare/ip-interface...lucaszanella:ip-interface?expand=1, you'll see several modifications. I sucessfully made a VirtualTun device that receives packets from a queue instead of a file descriptor, and sends to a queue (queue part to be done but the device receives the packet already). I'm working with example packets to debug things since it's going to take time for me to integrate this to OpenVPN.

So what you said you would do is kinda done, I just need to refactor + reactivate the tests that I commented.

Now you'd only have to create a real Tun device instead of using my VirtualTun one.

What you think? Can you make it so we can test my modifications better?

again: code is ugly, gonna refactor a lot

@lattice0
Copy link
Contributor

Ok so I made the tun interface also. So now the project has a tap interface, a tun interface, and a virtual tun interface (reads packets from memory instead of from file descriptor).

I used Wireshark and could see the packet being sent to the tun device I created. However, I don't know how to connect this device to the world, so I can sucessfully test the HTTP client example. Right now I can see that it sends a TCP SYN but since the tun device is not connected to anything, it wont respond.

Any ideas on how to test it?

My changes: https://github.com/Dirbaio/smoltcp/compare/ip-interface...lucaszanella:ip-interface?expand=1

only thing missing now are mod tests that I commented + refactoring

@lattice0
Copy link
Contributor

I made the tunhttpclient example work!

sudo ip tuntap add dev tun1 mode tun user `id -un`
sudo ip link set dev tun1 up
sudo ip addr add dev tun1 local 192.168.69.0 remote 192.168.69.1
sudo iptables -t filter -I FORWARD -i tun1 -o eth0 -j ACCEPT
sudo iptables -t filter -I FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
sudo iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
sudo sysctl net.ipv4.ip_forward=1

./tunhttpclient 172.217.28.238 http://google.com

it sucessfully retrivies HTML from google using the tun interface :)

@lattice0
Copy link
Contributor

Great news!

tun example working and all non ethernet tests working!

https://github.com/Dirbaio/smoltcp/compare/ip-interface...lucaszanella:ip-interface?expand=1

Just need to know if process_ndisc is ethernet related or not. Anyone?

After this, I can refactor the code to take comments out and then pull request to @Dirbaio's code

@whitequark
Copy link
Contributor

Just need to know if process_ndisc is ethernet related or not. Anyone?

NDISC is the equivalent of ARP, AFAIK you don't need it for TUN interfaces (but I didn't check in depth).

@lattice0
Copy link
Contributor

thanks @whitequark, however it was not simple to remove ndisc. see:

I refactored everything. There are just 2 issues before I push to @Dirbaio's code:

Here:

https://github.com/lucaszanella/smoltcp/blob/071e416320cb2303e1c2b886550fc4169b3559f2/src/iface/tun.rs#L476

if I pass a lower that does not implement ip::LowerProcessor it won't compile because of the trait bound ip::LowerProcessor for the lower argument. This is the trait that requires process_ndisc so I left like this:

    fn process_ndisc<'frame>(&mut self, timestamp: Instant, ip_repr: Ipv6Repr,
                             repr: NdiscRepr<'frame>) -> Result<Option<ip::Packet<'frame>>> {
        self._processor.process_ndisc(timestamp, ip_repr, repr)
    }

where process_ndisc does nothing:

    fn process_ndisc<'frame>(&mut self, _timestamp: Instant, _ip_repr: Ipv6Repr,
        _repr: NdiscRepr<'frame>) -> Result<Option<ip::Packet<'frame>>> {
            Ok(None)
    }

and here:

https://github.com/lucaszanella/smoltcp/blob/071e416320cb2303e1c2b886550fc4169b3559f2/src/iface/tun.rs#L429

there's no Packet in wire/ip.rs just as there's EthernetFrame in wire/ethernet.rs

The problem is that I can't implement

impl<T: AsRef<[u8]>> PrettyPrint for Frame<T> {

in order to pretty print the packet in that line.

What should be done here? I see that there's methods to pretty print each individual packet (ipv6, igmp, udp, tcp etc) but there's no trait implementation for IP.

@lattice0
Copy link
Contributor

Hello, finally a pull requested to @Dirbaio's repo :)

https://github.com/Dirbaio/smoltcp/pull/1/files

@Dirbaio
Copy link
Member Author

Dirbaio commented Jun 3, 2020

@LucasZanella that looks great! Thanks for putting the work.

Sorry everyonw for the long delays adressing questions and review.

I've been integrating smoltcp with my changes into our product's firmware, and I've found some concerns about the current implementation.

Concern 1: binary size bloat

Right now most of the ip code is generic over the LowerProcessor trait, which means they're emitted N times as binary code, monomorphized for each type of interface in use. This monomorphization propagates to all the sockets' dispatch methods because they take a closure that uses the LowerProcessor. And all these methods are definitely not small :(

This means that if you want your firmware to support ethernet and ip devices at the same time you're going to pay quite a bit of flash space penalty. This is our case: we want to make a single device, where a single firmware supports pluggable Ethernet/Wifi modules (Ethernet medium), and cellular modules (PPP, IP medium).

You may think this is already a problem with the current code due to the Device trait: if you have two Device implementations, you're going to have two iface::Ethernet monomorphizations. That's not actually the case: you can use dynamic dispatch to the actual device code, so all the smoltcp code is only instantiated once. It's a bit convoluted because Device is not object-safe, but it's doable.

Concern 2: maintainability, code duplication

As seen in the current EthernetInterface and @LucasZanella 's TunInterface, there's quite a bit of code duplication that I don't think is easy to remove.

The tests should be refactored so all IP-specific tests are in ip module, but there's still a lot of duplication left. I really can't think of a way to remove it :(

Concern 3: the Device trait is ambiguous

You can create a iface::TunInterface with a phy::TapInterface, it compiles with zero complaints but blows up spectacularly at runtime.

Alternative solution

I think I've found a way to solve these problems:

  • Add a medium method to Device, that returns Medium::Ethernet or Medium::IP.
  • Rename iface::Ethernet to iface::Interface (or similar. The point is that there won't be an interface struct per medium type)
  • Add checks to iface::Interface that check the medium of the device, and behave differently accordingly. For example, if medium is IP, send IP payloads to the device. If it's Ethernet, do ARP/NDISC and send Ethernet payloads to the device.

This solves the binary bloat nicely:

  • If you instantiate Interface with an Ethernet-only device, the medium method will return a constant of Medium::Ethernet. This will allow the compiler to inline it and statically remove all the checks, so the resulting code should be about as performant as the current one. Same with an IP-only device.
  • You can instantiate Interface with a device impl that does dynamic dispatch. This way you get only a single instantiation of all the smoltcp code, at the cost of it doing checks at runtime to see if it's dealing with an Ethernet or IP medium.

It also solves code duplication since there'll be just only one Interface struct.

I'm going to write a prototype of it. This will allow us to do code-size comparisons and decide.

@whitequark
Copy link
Contributor

I'm excited to see your solution. In general I think code duplication isn't necessarily bad but I agree that in this specific case it probably makes things harder to maintain!

@Dirbaio Dirbaio mentioned this pull request Jun 4, 2020
4 tasks
@Dirbaio Dirbaio changed the title Separate IP parts from EthernetInterface. Add support for IP mediums, v1 Jun 4, 2020
@Dirbaio Dirbaio marked this pull request as draft June 4, 2020 04:57
@Dirbaio
Copy link
Member Author

Dirbaio commented Jun 23, 2020

Closing this in favor of #346

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

5 participants