-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
base: main
Are you sure you want to change the base?
Conversation
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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]>
cb1fe0b
to
3420ba0
Compare
All in all, looks good to me 🚀 |
…mIpNameLookup Signed-off-by: Ryan Levick <[email protected]>
7085038
to
dd7a684
Compare
Signed-off-by: Ryan Levick <[email protected]>
I've moved the implementation towards having a single The current solution has the following charactertistics:
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( |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
Signed-off-by: Ryan Levick <[email protected]>
|
||
if !network.allow_ip_name_lookup { | ||
return Err(ErrorCode::PermanentResolverFailure.into()); | ||
impl SystemNetwork { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 aWasiCtx
- Leave it as is
I think I'd be most in favor of the last two options.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
Having thought about it some more I would like to offer one more suggestion. See code below. The main difference is that this setup doesn't modify WasiView at all, but instead dispatches through WasiCtx::network
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
}
} |
@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 Otherwise though I think it'd be reasonable to configure a trait object on |
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 |
Signed-off-by: Ryan Levick <[email protected]>
@alexcrichton @badeend I've pushed a new change that moves more towards @badeend's design as well as changing the return type of 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? |
Correct. In my mind that was exactly the goal of this (and upcoming) PRs.
Agree.
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 |
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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?
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. |
- BoxSyncFuture: for public facing signatures. This is just a regular `Pin<Box<dyn Future ...>>` - Preview2Future: utility type for use within the WASI wrapper code
Co-authored-by: Ryan Levick <[email protected]>
Co-authored-by: Ryan Levick <[email protected]>
…to tcp-override
TCP overrides
…nto tcp-override
…ocketAddrFamily anymore
Sync with upstream
Signed-off-by: Ryan Levick <[email protected]>
Signed-off-by: Ryan Levick <[email protected]>
Signed-off-by: Ryan Levick <[email protected]>
UDP override
@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 |
Sounds good to me! And yeah refactorings are always good to land at any time |
This follows the design outlined in #7681 for IpNameLookup.
A new trait
IpNameLookup
is introduced that factors out the conversion from aString
to a list of IP addresses. Another trait,WasiIpNameLookupView
is used to configure which concrete type that implementsIpNameLookup
will be used in the actual IpNameLookup host implementation. Lastly. theSystemIpNameLookup
is provided as a default implementation.Some questions that might guide tweaks:
WasiIpNameLookupView
- it would be nice if there was a to default to the built inSystemIpNameLookup
. If there were default associated types for traits, we could get rid ofWasiIpNameLookupView
and makeip_name_lookup
a function onWasiView
that defaults to the theSystemIpNameLookup
. But alas that's not available to us.r? @badeend