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

[WASI] Dynamic SocketAddr check for outbound socket traffic #7662

Merged
merged 3 commits into from
Dec 13, 2023

Conversation

rylev
Copy link
Contributor

@rylev rylev commented Dec 8, 2023

This PR adds the ability to do dynamic checks of whether to allow outbound socket traffic to a given SocketAddr. The checks are purely additive over the underlying cap_std::net::Pool. If the check returns true the traffic is allowed, while if it returns false, the underlying cap_std::net::Pool is then checked.

This also changes the WasiCtx::inherit_network function to not take an AmbientAuthority since all other methods on WasiCtx do not take this value.

The user can now specify a closure which gets run any time a connection
to an external address is attempted. If the closure returns true, the
address is allowed, but if it returns false, the underlying pool is then
checked. In otherwords, false does not imply denied, but rather, check
the underlyin pool.

Signed-off-by: Ryan Levick <ryan.levick@fermyon.com>
@rylev rylev requested a review from a team as a code owner December 8, 2023 17:29
@rylev rylev requested review from alexcrichton and removed request for a team December 8, 2023 17:29
@github-actions github-actions bot added the wasi Issues pertaining to WASI label Dec 8, 2023
@alexcrichton
Copy link
Member

Could you expand more on the use case for this? Pool seems to be trying to be pretty full-featured to avoid the need for a dynamic check such as this, so ideally we'd lean on that. This design also raises questions such as what happens when the method is called twice (overwriting seems a bit surprising to me) and what to do when false is returned (falling back on other logic seems non-obvious to me).

@rylev
Copy link
Contributor Author

rylev commented Dec 11, 2023

@alexcrichton sorry I should have been more clear with use cases this supports! I see several use cases for this:

Use cases

Convenience for complex checks

Some checks are easy to express in code but complicated to express as blocks of IP addresses. For example, allowing only non-private IP v4 network traffic is trivial as a predicate function but really difficult as a collection of CIDR blocks:

ctx.dynamic_socket_addr_check(|a| match a.ip() {
    IpAddr::V4(i) => !i.is_private(),
   _ => false
});

Dynamic checks that change over time

Typically the Pool of IP addresses that are supported is instantiated before any component is run. This means that more dynamic use cases are either not possible or much hard to achieve. For example, allowing the environment to control some aspects of what IP addresses are allowed:

ctx.dynamic_socket_addr_check(|a| {
    if let Ok(true) = std::env::var("ALLOW_LOOPBACK")
       a.ip().is_loopback()
    } else {
      false
   }
});

Concerns

Supporting directly in Pool

I agree that adding this support to Pool might make more sense but how exactly to do that does not seem super straight forward. Since the only user facing change here is the dynamic_socket_addr_check, we could potentially punt on adding this to Pool if we're happy with the user facing API.

Multiple Calls

I agree, it's a bit strange that calling this function multiple times overrides the previous calls. There is some precdent of this already in that WasiCtx::inherit_network sort of overrides previous invocations though one could argue that it doesn't invalidate them, it just makes them redundant. Would storing these callbacks in a Vec or SmallVec make sense and be worth the cost of potentially more allocation?

Behavior of false

I understand that false meaning "do other checks" isn't the most intuitive. It does make sense if the predicate is thought of only as "are we allowing this traffic" vs "are we allowing or denying this traffic". The status quo is the former of these two options and mirrors the behavior of Pool (i.e., adding an IP range to Pool does not mean that if an IP falls out of that range, the traffic will be denied. There may be some other range that allows that IP.).

We could work around this completely by changing the closure to return a return type that is a bit more explicit:

enum IpCheck {
  // This is the equivalent of returning `true` in current scheme
  Allow,
  // Being able to deny explicitly in these dynamic checks is new
  Deny,
  // This is the equivalent of returning `false` in current scheme
  Passthrough
}

This makes these more explicit at the cost of ultimately be more complicated (each closure has three possible outcomes as opposed to two).

@alexcrichton
Copy link
Member

Ok those use cases make sense to me, and I can see how it's not quite as easy to map those to a Pool.

Perhaps though I could propose an alternative direction to this PR: what if Pool were removed entirely in favor of just a callback? That way a WasiCtx would store a single "is this IP address allowed" callback. That seems like it might be able to capture the best of both worlds:

  • No need to consider interactions of callbacks-and-Pool, there's only callbacks.
  • Users who want to keep using Pool still easily can.
  • No need to worry about multiple callbacks, there's only one (and it's documented as only one).

Does that sound reasonable?

@rylev
Copy link
Contributor Author

rylev commented Dec 11, 2023

Sounds reasonable to me. This will certainly make the API much simpler as there isn't as much of a need to mirror the Pool API on WasiCtx. I'll give that a try.

Signed-off-by: Ryan Levick <ryan.levick@fermyon.com>
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 good to me, thanks!

I also want to cc @badeend and @sunfishcode on this as I suspect one or both of y'all were involved in originally adding Pool. Wanted to make sure to run this by you to double-check that I'm not missing anything by considering this a workable alternative.

@badeend
Copy link
Contributor

badeend commented Dec 12, 2023

I think decoupling the network policies from the actual syscall implementations is a good thing. I already ran into the limitations of Pool and PoolExt multiple times myself. Which is (one of) the reasons why the current implementation ended up using rustix' connect and tokio's send & recv directly, instead of going through cap-std's connect_existing_udp_socket & send_to_udp_socket_addr.

At the same time, this change throws every address on one big pile (called check) and looses the context in which the address is about to be used. Eg. inbound vs outbound, local vs remote, etc. Not a big problem for now, but maybe this could be extended in the future with check_for_tcp_bind, check_for_tcp_connect, check_for_udp_send, ..

@sunfishcode
Copy link
Member

When I wrote Pool, my thought was that having the logic for IP namespace sandboxing factored out into an external crate would make that code easier to test and audit. My idea was that we'd replace Pool's initial simplistic implementation once we knew what we needed. In practice, it seems it's been more straightforward to just implement the logic in Wasmtime rather than to modify Pool. In theory I may look into factoring out the code we write in Wasmtime back out into an external crate some day, but I don't think that's something we need to block on here.

@alexcrichton
Copy link
Member

Perhaps a second argument could be added to the callback along the lines of:

enum Reason {
    TcpBind,
    TcpConnect,
    UdpSend,
    ...
}

to help distinguish why the callback is being invoked?

@rylev
Copy link
Contributor Author

rylev commented Dec 12, 2023

I've added a SocketAddrUse argument to the closure check. Let me know if that looks good including naming.

@badeend
Copy link
Contributor

badeend commented Dec 13, 2023

Looks fine to me 👍

@alexcrichton alexcrichton added this pull request to the merge queue Dec 13, 2023
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Dec 13, 2023
Signed-off-by: Ryan Levick <ryan.levick@fermyon.com>
@alexcrichton alexcrichton added this pull request to the merge queue Dec 13, 2023
Merged via the queue into bytecodealliance:main with commit 2ac2015 Dec 13, 2023
19 checks passed
@rylev rylev deleted the dynamic-socket-addr-check branch December 13, 2023 19:44
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.

4 participants