Skip to content

Instantly share code, notes, and snippets.

@alevy
Created March 6, 2019 17:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alevy/1218d017a107073c39b47daeb33fbb2b to your computer and use it in GitHub Desktop.
Save alevy/1218d017a107073c39b47daeb33fbb2b to your computer and use it in GitHub Desktop.
Port Binding Design Document

Network Security/Virtualization Design Document

This document describes the network security (and virtualization) goals for the 6LoWPAN stack on Tock.

Notably, this document does not encompass any discussion of BLE. Further, as the current 6LoWPAN stack only includes UDP as a transport layer, it does not include explicit discussion of TCP usage, though the design in this document should not preclude the use of this policy for TCP once implemented.

Goals

  1. Allow for virtualized use of network resources by both userspace applications and kernel applications (e.g. capsules) with minimal performance overhead, while preventing applications from overhearing one another.

    • Enforce port binding for UDP transmission and reception. Once an application has bound to a UDP socket (combination of interface and port) no other application can bind to that port until the original application has unbound that socket.

    • Applications (userspace or capsules) can only receive packets on ports to which they are bound. Applications can only send packets from ports to which they are bound. This prevents eavesdropping and impersonation.

    • Userspace applications are only able to bind to a single port at a time. Capsules can bind to multiple, but at a resource cost.

  2. Allow for the enforcement of diverse network security policies such as:

    • "Capsule A cannot listen to the radio"

    • "Capsule A cannot use the socket interface"

    • "Capsule C cannot send data without first encrypting it"

    • "Capsule B can only communicate with hosts on .amazon.com domains"

    • "Capsule D can communicate with any external node on the link-local network, but no external nodes"

  3. Network security / virtualization should have a low cost (code size, RAM) relative to the total size of the network stack.

  4. Auditing for network security should require no inspection of code outside of kernel/, main.rs, networking components (e.g. components/udp_6lowpan.rs) and capsules/net. This means that safely adding an external capsule requires only writing a component to load that library and passing the appropriate capabilities to that component. Similarly, auditing the capabilities of a userspace application should require only auditing its capability manifest.

Design

Port Binding

  • Port Bindings are stored in an array in the kernel which maps indexes to bound ports. To limit the size required for this array, we include a limit to the total number of ports that can be bound to simultaneously. By default, we set this value to 16.

  • Only one instance of this mutable array exists, and it is contained in a TakeCell inside an immutable struct - struct UdpPortTable. The interior array can only be modified by a small set of functions that enable binding and unbinding to the table. These functions take in a UdpPortSocket, so only capsules capable of creating sockets can even attempt to use these function. Further, these functions enforce binding/unbinding rules, such that no two applications can ever be bound to the same port.

  • An application binds to a port by creating a socket (UdpPortTable.create_socket()), and calling UdpPortTable.bind(socket) to get a UdpPortBinding.

// An opaque descriptor that allows the holder to receive UDP packets on a port
pub struct UdpReceiverBinding<'a> {
    port: u16,
}

// An opaque descriptor that allows the holder to send UDP packets from a port
pub struct UdpSenderBinding<'a> {
    port: u16,
}

pub struct UdpPortBinding<'a> {
    receive_allocated: Cell<bool>, // Has the associated rx_binding been given out?
    send_allocated: Cell<bool>, // Has the associated tx_binding been given out?
    //rx_binding: &'a UdpReceiverBinding, // Reference to the RX binding associated with this port binding
    //tx_binding: &'a UdpSenderBinding,
    socket: UdpPortSocket,
    port: u16,
    table_ref: &'static UdpPortTable,
}

impl UdpPortBinding<'a> {
  pub fn get_receiver(&self) -> Result<UdpReceiverBinding, ()> {
      if self.receive_allocated.get() {
         Err(())
      } else {
          self.receive_allocated.set(true);
          Ok(UdpReceiverBinding { port: self.port })
      }
  }

  pub fn put_receiver(&self, recv_binding: UdpReceiverBinding)
      -> Result<(), UdpReceiverBinding> {
      if recv_binding.port == self.port {
          self.receive_allocated.set(false);
          Ok(())
      } else {
          Err(recv_binding)
      }
  }
}
  • As the above code shows, the UdpPortBinding can be used to generate a UdpSenderBinding and a UdpReceiverBinding. These structs simply hold a single port. A capsule which holds one of these bindings must pass it to send on / set up a receive callback for the port held in the Udp{Sender,Receiver}Binding. These structures are defined in the kernel, and the interior fields of these structs are not visible outside of the module in which they are defined, so a capsule cannot modify the port of a Binding that it holds. The inclusion of Sender/Receiver bindings in addition to the all-encompassing port binding object is to addresses cases where different portions of an application may need to handle sending and reception, or to make it possible for a capsule to distribute these capabilities separately. This design also makes future support for a capabilities that forbid certain capsules from either sending or reception easy to implement.

  • While further description of the virtualization infrastructure will be added to this document in the future, for now it is sufficient to note that the UDP driver will have to hold one UdpPortBinding for every application currently on the device. To enable this without unnecessary waste of RAM, we plan to store these bindings in the grant region of each app.

  • To prevent dynamically removed (or faulted) apps from using up valuable slots in the UdpPortTable, we are implementing a drop trait for the UdpPortBinding which will free its slot in the UdpPortTable whenever a binding goes out of scope. This is slightly different from the usual unBind(), which will only succeed if the UdpSenderBinding and UdpReceiverBinding have been returned.

The Problem

There exists a significant problem with the design described so far -- what if a UdpPortBinding goes out of scope, but the associated UdpSenderBinding and UdpReceiverBinding are still valid? This would free the slot in the table (via the UdpPortBinding drop trait) while some entity was still using those slots, breaking our guarantees of exclusive access.

Potential Solutions - Help Wanted
  1. Don't let the drop trait free the slot in the table if either the sender or receiver is allocated. This is problematic because adding and removing apps over time could lead to the port table being saturated.

  2. Somehow associate the lifetime of the UdpSenderBinding and UdpReceiverBinding with the lifetime of the UdpPortBinding such that these children cannot outlive the parent. Perhaps PhantomData or PhantomRef can be used towards this purpose somehow?

  3. Give the UdpPortBinding references to its child bindings, and modify UdpSenderBinding and UdpReceiverBinding such that they contain a "valid" field that can be used to mark them invalid when the UdpPortBinding goes out of scope. This is difficult because the UdpPortBinding creates the child bindings that it will ultimately hold references to, so the references would need to be inside an Option<> such that they could be null before the child bindings are created. I am not sure exactly how we would create a reference to a held object before passing that held object on, but perhaps it is possible? This also means that all uses of the child bindings would have to correctly check this valid field.

  4. Have bind() create all 3 bindings seperately with the same lifetime, and pass them back as a tuple. But then there would be no way to return the child bindings to the parent, which defeats the purpose of this design.

An additional problem exists - what if the UdpSenderBinding or UdpReceiverBinding go out of scope while the UdpPortBinding remains in scope? In our case, this means that the ability to send or receive on that port is lost until the UdpPortBinding is destroyed by going out of scope. However, we think it is acceptable to place this burden on developers.

Capsule + userspace UDP virtualization

TODO: Describe virtualization layers for UDP tx and rx that allow for multiple capsule senders and multiple application senders through a single driver capsule. Ditto for receive.

Network Security Capabilities

We have not yet implemented a system to enforce the diverse security policies described above. However, our current plan is summarized below:

Plan

To enforce security policies at a relatively low cost, we intend to use a capability-based system, where applications/capsules must pass capabilities to any function calls which require permissions that are not enabled by default. All capabilities implement unsafe traits and thus can only be created in the kernel, so capsules can only obtain these capabilities when instantiated at boot or when passed a capability by code running in the trusted portion of the kernel. Basic capabilities contain no fields, and thus are enforced entirely at compile time, adding no code size or RAM. Some capabilities, such as those for limiting acceptable communication endpoints, require data be associated with the capability. These 'dynamic capabilities' contain data fields which can only be modified using the unsafe keyword.

Example Capabilities

  • UdpBindCapability - required to bind in the port table

  • UdpSendUnencryptedCapability - required to use a UDP sending interface which does not first encrypt traffic

  • UdpAllEndpointsCapability - If passed, this capability allows a capsule to send to any endpoints

  • UdpNatEndpointsCapability: (dynamic capability example)

    pub unsafe trait UdpNatEndpointsCapability {
        pub fn can_send_to(self, endpoint: & [u8]) -> bool;
    }
    
    struct myTrustedEndpoints<'static> {
      endpoint1: &'static [u8]; //immutable buffer contains 'apple.com'
      endpoint2: &'static [u8];
      endpoint3: &'static [u8];
      endpoint4: &'static [u8];
      endpoint5: &'static [u8];
    
    }
    
    unsafe impl UdpNatEndpointCapability for myTrustedEndpoints {
        pub fn can_send_to(self, endpoint: & [u8]) {
            // check if endpoint in passed buffer is one of 5 allowable endpoints.
            // if so, return true, else, return false.
        }
    }
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment