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

Fine-grained network policies #7705

Open
wants to merge 46 commits into
base: main
Choose a base branch
from

Conversation

rylev
Copy link
Contributor

@rylev rylev commented Dec 19, 2023

This follows the design outlined in #7681 for IpNameLookup.

A new trait IpNameLookup is introduced that factors out the conversion from a String to a list of IP addresses. Another trait, WasiIpNameLookupView is used to configure which concrete type that implements IpNameLookup will be used in the actual IpNameLookup host implementation. Lastly. the SystemIpNameLookup is provided as a default implementation.

Some questions that might guide tweaks:

  • Users are now forced to implement WasiIpNameLookupView - it would be nice if there was a to default to the built in SystemIpNameLookup. If there were default associated types for traits, we could get rid of WasiIpNameLookupView and make ip_name_lookup a function on WasiView that defaults to the the SystemIpNameLookup. But alas that's not available to us.

r? @badeend

@rylev rylev requested a review from a team as a code owner December 19, 2023 15:48
@rylev rylev requested review from alexcrichton and removed request for a team December 19, 2023 15:48
fn resolve_addresses(
&mut self,
network: Resource<Network>,
name: String,
) -> Result<Resource<ResolveAddressStream>, SocketError> {
let network = self.table().get(&network)?;

let host = parse(&name)?;

if !network.allow_ip_name_lookup {
Copy link
Contributor

Choose a reason for hiding this comment

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

With these changes, can network.allow_ip_name_lookup now be removed?

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 could be potentially. Right now the default is to not allow ip name lookups, so if we remove this, the default will flip. Additionally, the new way to turn off name lookups would be through a trait implementation (i.e., impl IpNameLookup for BlockedIpNameLookup or similar) and not through WasiCtx/WasiCtxBuilder.

Though it's probably somewhat confusing to implement WasiIpNameLookupView and then not have name lookups work because you forgot to flip the bit on WasiCtx.

Perhaps we remove the allow_ip_name_lookup and provide an additional implementation of IpNameLookup: BlockedIpNameLookup so that users can easily turn off IP name lookup if they like. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm. Interesting.
I think the WASI translation layer (preview2/ip_name_lookup.rs in this case) should only depend on a single source of truth: WasiIpNameLookupView. It should not also go peeking into network.allow_ip_name_lookup on its own.

It might be an idea to provide an additional implementation of IpNameLookup based on WasiCtx, that dynamically checks allowed_network_uses. This could supersede the BlockedIpNameLookup you suggested

Copy link
Contributor Author

@rylev rylev Dec 19, 2023

Choose a reason for hiding this comment

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

This does have the downside that the WasiCtx setting can be ignored by users. If their implementation of IpNameLookup doesn't check for allowed_network_uses, that option will simply be ignored. I guess it seems fine that the user can ignore that option (they're the ones setting it anyway), but it does seem somewhat error prone.

In any case, I pushed a commit that moves the check into SystemIpNameLookup.

A new trait `IpNameLookup` is introduced that factors out the conversion
from a `String` to a list of IP addresses. Another trait,
`WasiIpNameLookupView` is used to configure which concrete type that
implements `IpNameLookup` will be used in the actual IpNameLookup host
implementation. Lastly. the `SystemIpNameLookup` is provided as a
default implementation.

Signed-off-by: Ryan Levick <[email protected]>
@badeend
Copy link
Contributor

badeend commented Dec 19, 2023

All in all, looks good to me 🚀

@github-actions github-actions bot added the wasi Issues pertaining to WASI label Dec 19, 2023
@rylev
Copy link
Contributor Author

rylev commented Jan 4, 2024

@badeend @alexcrichton

I've moved the implementation towards having a single WasiNetworkView trait where all network based overrides will be implemented. I'm happier with this solution than the previous IpNameLookupView version, but I'm still not convinced we've arrived at a good solution.

The current solution has the following charactertistics:

  • Two new methods on WasiView: network_view and network_view_mut which return references to a dyn WasiNetworkView
    • This prevents all the methods necessarily being on WasiView which might get crowded over time. It also makes it easy for users who want to use an off-the-shelf solution to get implement 2 methods instead of the N methods WasiNetworkView will have.
    • The use of dynamic dispatch is new to WasiView but seems appropriate here.
  • The various implementors in Wasmtime of WasiView gain a new field with SystemNetwork (the default implementation of WasiNetworkView)

I would said, we move forward with this for now and we can adjust as we add TCP and UDP support (and of course continue to evolve over time). Thoughts?

impl<T: WasiView> Host for T {
/// A trait for providing a `IpNameLookup` implementation.
pub trait WasiNetworkView: Send {
/// Given a name, resolve to a list of IP addresses
fn resolve_addresses(
Copy link
Member

Choose a reason for hiding this comment

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

This might be best to model as an async function rather than returning an AbortOnDropJoinHandle?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is indeed very annoying... The value returned from resolve_address must be Send + Sync as it is used in ResolveAddressStream which impls Subscribe which requires the type implementing it to be Send + Sync.

The #[async_trait] macro puts a Send bound on the return type but not Sync and there is no way to instruct it to do so. We could move to using Box<dyn Future> but that means we loose the developer experience of async_trait. We could potentially move to using impl Future but that requires the latest version of Rust, and I'm not sure if that's ok.

Copy link
Member

Choose a reason for hiding this comment

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

Hm that's true, and yeah I see how there's no great way to manage this right now.

I suppose this can stay for now, but I'm still worried that this will inevitably cause some pain down the road. The best alternative I can think of is to return a oneshot::Receiver<T> instead but that's still pretty cumbersome to work with and wouldn't work great here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could take a page out of WasiHttpView's playbook and have the function return a wasmtime::Result<Resource<ResolveAddressStream>> just like WasiHttpView::send_request returns a wasmtime::Result<Resource<HostFutureIncomingResponse>>. I don't really love this either, but at least it's consistent?

Copy link
Contributor

Choose a reason for hiding this comment

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

Of all the options mentioned so far, the Box<dyn Future> route seems the least worst to me.

The fact that we currently use a quick&dirty implementation using spawn_blocking should be an implementation detail of SystemNetwork

Copy link
Member

Choose a reason for hiding this comment

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

Returning a Resource<ResolveAddressStream> would be nice yeah but it seems like that would have all the same design questions around "how do you actually create one of those".

Although as I say that we could perhaps get around async trait by having something like fn from_future<T: Future + Send + Sync + 'static> (...) on the ResolveAddressStream type. That way you could pass an async block which would have bounds generated correctly and additioanlly the handle from spawn_blocking could also be passed in? That way we might be able to avoid async traits altogether and rely entirely on that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking about this more, returning Resource<ResolveAddressStream> would mean that the implementor would have to also implement WasiCtx or otherwise have multiple access to the ResourceTable. Right now we don't require that from the implementor of WasiNetworkView, and I'm not sure we want to.

Copy link
Member

Choose a reason for hiding this comment

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

ResolveAddressStream itself could be returned though leaving the table management bits to the glue?


if !network.allow_ip_name_lookup {
return Err(ErrorCode::PermanentResolverFailure.into());
impl SystemNetwork {
Copy link
Member

Choose a reason for hiding this comment

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

Could this be removed in lieu of implementing WasiNetworkView for WasiCtx itself? (given that WasiCtx is needed to create this structure anyway it may not buy a whole lot)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If a user wants to have some logic where they conditionally switch between either their own network implementation and SystemNetwork, they need some way of constructing the SystemNetwork. Without this constructor, they would not be able to do so. We could do the following:

  • make all fields of SystemNetwork public
  • Make the constructor just take a simple bool instead of a WasiCtx
  • Leave it as is

I think I'd be most in favor of the last two options.

Copy link
Member

Choose a reason for hiding this comment

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

Hm I'm not sure I understand? Given impl WasiNetworkView for WasiCtx then if users are themselves implementing WasiNetworkView for their own custom type then conditionally using the default behavior would be calling self.ctx.the_method_name() so I'm not sure where the need for having this separate SystemNetwork structure arises?

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're right that SystemNetwork isn't strictly necessary. It felt nice to have a distinct type that lives in the ip_name_lookup module that can handle name lookups, but you're right that we can just implement that logic directly on WasiCtx. It does make the code a wee bit simpler.

impl<T: WasiView> Host for T {
/// A trait for providing a `IpNameLookup` implementation.
pub trait WasiNetworkView: Send {
/// Given a name, resolve to a list of IP addresses
fn resolve_addresses(
Copy link
Member

Choose a reason for hiding this comment

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

Hm that's true, and yeah I see how there's no great way to manage this right now.

I suppose this can stay for now, but I'm still worried that this will inevitably cause some pain down the road. The best alternative I can think of is to return a oneshot::Receiver<T> instead but that's still pretty cumbersome to work with and wouldn't work great here.

@badeend
Copy link
Contributor

badeend commented Jan 6, 2024

Having thought about it some more I would like to offer one more suggestion. See code below.
This touches multiple discussions above, so tagging @alexcrichton.

The main difference is that this setup doesn't modify WasiView at all, but instead dispatches through WasiCtx::network


@rylev: This does have the downside that the WasiCtx setting can be ignored by users.

In setup below that can't happen.


The strict separation between the "System*" and "Default*" structs below might be overkill for this PR because of the relative trivial "System*" and "Default*" implementations. However, for TCP/UDP sockets the implementation won't be so trivial anymore.

/// Trait for any WASI-compliant network implementation.
trait Network {
    fn resolve_addresses(&mut self, ) -> ...;

    // fn new_tcp_socket(family) -> ...
    // fn new_udp_socket(family) -> ...
}




/// A WASI-compliant "base" implementation.
/// Without any opinions on how to do filtering, permission checks or whatever.
struct SystemNetwork {}
impl SystemNetwork {
    fn new() -> Self {} // Probably no parameters
}
impl Network for SystemNetwork {
    fn resolve_addresses(&mut self, ) -> {
        // Perform the syscalls without any additional permission checks
    }
}




/// A "good enough" default implementation for Wasmtime users, _with_ permission checks.
struct DefaultNetwork {
    sys: SystemNetwork,
    allowed: bool
}
impl DefaultNetwork {
    /// Whatever parameters we want.
    /// Currently just a simple boolean, but could be extended to any 
    fn new(allowed: bool) -> Self {}
}
impl Network for DefaultNetwork {
    fn resolve_addresses(&mut self, ) -> {
        if self.allowed {
            self.sys.resolve_addresses()
        } else {
            Err(PermissionDenied)
        }
    }
}



pub struct WasiCtx {
    pub(crate) random: Box<dyn RngCore + Send + Sync>,
    pub(crate) insecure_random: Box<dyn RngCore + Send + Sync>,
    pub(crate) insecure_random_seed: u128,
    pub(crate) wall_clock: Box<dyn HostWallClock + Send + Sync>,
    pub(crate) monotonic_clock: Box<dyn HostMonotonicClock + Send + Sync>,
    pub(crate) env: Vec<(String, String)>,
    pub(crate) args: Vec<String>,
    pub(crate) preopens: Vec<(Dir, String)>,
    pub(crate) stdin: Box<dyn StdinStream>,
    pub(crate) stdout: Box<dyn StdoutStream>,
    pub(crate) stderr: Box<dyn StdoutStream>,

    // Remove `socket_addr_check`
    // Remove `allowed_network_uses`
    
    // Add:
    pub(crate) network: Box<dyn Network>,
}





pub struct WasiCtxBuilder {
    // (...)

    network: Box<dyn Network>,
}
impl WasiCtxBuilder {
    pub fn new() -> Self {
        Self {
            // (...)
            network: DefaultNetwork::new(/*allowed: */false),
        }
    }

    pub fn allow_network(&mut self, enable: bool) -> &mut Self {
        self.network = DefaultNetwork::new(enable);
        self
    }

    pub fn custom_network(&mut self, net: Box<dyn Network>) -> &mut Self {
        self.network = net;
        self
    }
}

@alexcrichton
Copy link
Member

@badeend your sketch makes sense yeah, but my thoughts at #7694 (comment) showcase how something like that breaks down unless all callbacks about configuration go through trait Network, so for example we'd also need to route IP address checks through that trait as well. (not a problem of course, just pointing out that once things are behind a trait everything needs to be behind that trait to share state between callbacks).

Otherwise though I think it'd be reasonable to configure a trait object on WasiCtx vs returning a &mut dyn Trait from WasiView (although it having a default impl returning self.ctx_mut() I think is pretty nifty and makes it almost hidden.

@rylev
Copy link
Contributor Author

rylev commented Jan 8, 2024

I like @badeend's suggestion. I initially shared the same concern that @alexcrichton pointed out, but I think it should be relatively easy for users to implement the Network trait by delegating all the parts they don't care about to SystemNetwork or DefaultNetwork and only changing what they do care about. The only issue I can see with this is that if the Network trait grows large (which I think we anticipate it will), then there will be a large amount of boiler plate of delegating Network methods to the SystemNetwork or DefaultNetwork. This could be really annoying.

Signed-off-by: Ryan Levick <[email protected]>
@rylev
Copy link
Contributor Author

rylev commented Jan 8, 2024

@alexcrichton @badeend I've pushed a new change that moves more towards @badeend's design as well as changing the return type of resolve_addresses to be a ResolveAddressStream.

I think the only way for us to be sure about this design is to keep going and implement UDP and TCP on top of it. With that in mind, I think we should merge this PR and continue the design exploration in a follow up. Thoughts?

@badeend
Copy link
Contributor

badeend commented Jan 8, 2024

something like that breaks down unless all callbacks about configuration go through trait Network, so for example we'd also need to route IP address checks through that trait as well.

Correct. In my mind that was exactly the goal of this (and upcoming) PRs. socket_addr_check & allowed_network_uses would most likely be moved to DefaultNetwork

I think the only way for us to be sure about this design is to keep going and implement UDP and TCP on top of it.

Agree.

With that in mind, I think we should merge this PR and continue the design exploration in a follow up. Thoughts?

I'm not familiar with wasmtime's release process, but if a release gets cut partway through these changes it could be weird for consumers when e.g. IP lookup & TCP use the Network trait but UDP still uses an old & completely different customization mechanism. I'll leave this up for @alexcrichton

Copy link
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

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

Looks reasonable to me!

Unless this is particularly urgent though I would personally agree with @badeend in that I'd prefer to see a more complete picture before landing if possible. If this is needed to unblock work I think it's ok to land, but if you're ok working with a fork for now though I think it'd be best to sketch out the other checks you're thinking of implementing to be able to see them altogether at once.

}

#[async_trait::async_trait]
impl<T: WasiView> Host for T {
impl<T: WasiView + Sized> Host for T {
Copy link
Member

Choose a reason for hiding this comment

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

I think the + Sized may not be necessary here?

@rylev
Copy link
Contributor Author

rylev commented Jan 9, 2024

I'm fine with not landing this on main. I just think we need to decide which fork/branch is the integration point so that if @badeend and I continue the work, we can bring that all together into one unified branch that will ultimately be merged into main.

@rylev rylev changed the title Abstract out IpNameLookup core functionality so that users can override it Fine-grained network policies Jan 16, 2024
@badeend
Copy link
Contributor

badeend commented Feb 11, 2024

@alexcrichton One of the (many..) changes in this PR is that the actual socket code has been separated out from the component trait implementation to its own SystemTcpSocket/SystemUdpSocket. To reduce the size of this PR, do you mind if I split that refactor off from this PR and submit a new one with just those changes? These changes will only be internal reshuffling of code, without affecting the public interface.

@alexcrichton
Copy link
Member

Sounds good to me! And yeah refactorings are always good to land at any time

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

Successfully merging this pull request may close these issues.

3 participants