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

Support HTTP Upgrade request for stealer. #928

Closed
meowjesty opened this issue Jan 10, 2023 · 1 comment
Closed

Support HTTP Upgrade request for stealer. #928

meowjesty opened this issue Jan 10, 2023 · 1 comment
Assignees
Labels
enhancement New feature or request

Comments

@meowjesty
Copy link
Member

The HTTP traffic filter doesn't currently support handling of Connection: upgrade requests.

We need this mostly for Upgrade: websocket.

@meowjesty meowjesty added the enhancement New feature or request label Jan 10, 2023
@meowjesty meowjesty self-assigned this Jan 10, 2023
bors bot pushed a commit that referenced this issue Jan 22, 2023
…led. (#973)

Issue: #928 

Depends on [test-images #14](metalbear-co/test-images#14).

Implements a pass-through mechanism to handle HTTP/1 upgrade requests from a client.

## Motivation

Currently, if you run mirrord with steal traffic, and HTTP filter enabled, we end up not serving HTTP upgrade requests appropriately. Our implementation of [`Service`](https://docs.rs/hyper/1.0.0-rc.2/hyper/service/trait.Service.html) would either:

- Forward the HTTP request to the local app (if a filter matched), send back a response and **not** upgrade the connection, or;
- Pass the request to its original destination, get a response back from it, and, again, **not** upgrade the connection.

This would prevent the user from handling websockets (our main motivator behind this feature), that start up as HTTP requests.

## Implementation

We're manually handling connection upgrades, by first checking if an HTTP request contains the "Upgrade" header, and if that's the case, then we:

- Current `unmacthed_request` flow:

1. Create a connection to the original destination (let's call this the `agent_original` connection);
2. Forward the HTTP request to it, awaiting its response;
3. Send the response back through our hyper connection (the `agent_client` connection we handle in hyper);

- The new steps:

4. Send the `agent_original` connection to the `filter_task` (through the `upgrade_tx` channel);
5. Retrieve the `agent_client` connection from hyper, as a normal `TcpStream`;
6. Copy whatever bytes were leftover from these connections (bytes that were read, but not processed by hyper);
7. Use [`copy_bidirectional`](https://docs.rs/tokio/latest/tokio/io/fn.copy_bidirectional.html) to keep copying from one connection to the other.

### Tests

The feature is being tested by `test_websocket_upgrade`, which is very similar to the `test_complete_passthrough` test. The main difference is that we use [`tokio-tugstenite`](https://docs.rs/tokio-tungstenite/latest/tokio_tungstenite/) to create a websocket connection (instead of a normal `TcpStream`).

## Alternatives

### Give a [`DuplexStream`](https://docs.rs/tokio/latest/tokio/io/struct.DuplexStream.html) to hyper

We could feed hyper a `DuplexStream` rather than the actual `TcpStream` connection, and deal with the upgrade ourselves, by parsing the bytes into an `Header`, checking for the "Upgrade: " header, or even use the [`Response`] as a way to identify that this was an upgrade request.

I've explored this approach, but this seemed a bit too much, especially when @t4lz asked me about "why not use the `unmatched_request`?"

### Use [`hyper::upgrade::on`]

hyper has a proper handler for dealing with HTTP upgrade requests, where you give it a [`Request`](https://docs.rs/hyper/1.0.0-rc.2/hyper/struct.Request.html) that contains an [`OnUpgrade`](https://docs.rs/hyper/1.0.0-rc.2/hyper/upgrade/struct.OnUpgrade.html) extension, and it'll give you back its connection (`agent_client`) and the leftover bytes.

I could not make this approach work, as we need to give the request to [`SendRequest`](https://docs.rs/hyper/1.0.0-rc.2/hyper/client/conn/http1/struct.SendRequest.html) by value, making it impossible to also feed it to the `upgrade::on` function. This was the case because we need to poll the connection (`agent_original`) in a separate task, thus requiring us to also move the request into it.

The request cannot be copied/cloned, as it contains a `Receiver` channel for the `OnUpgrade` feature, and if you try to manually copy it, the upgrade is never triggered, because we're dealing with different channels.

Co-authored-by: meowjesty <[email protected]>
bors bot pushed a commit that referenced this issue Jan 23, 2023
…led. (#973)

Issue: #928 

Depends on [test-images #14](metalbear-co/test-images#14).

Implements a pass-through mechanism to handle HTTP/1 upgrade requests from a client.

## Motivation

Currently, if you run mirrord with steal traffic, and HTTP filter enabled, we end up not serving HTTP upgrade requests appropriately. Our implementation of [`Service`](https://docs.rs/hyper/1.0.0-rc.2/hyper/service/trait.Service.html) would either:

- Forward the HTTP request to the local app (if a filter matched), send back a response and **not** upgrade the connection, or;
- Pass the request to its original destination, get a response back from it, and, again, **not** upgrade the connection.

This would prevent the user from handling websockets (our main motivator behind this feature), that start up as HTTP requests.

## Implementation

We're manually handling connection upgrades, by first checking if an HTTP request contains the "Upgrade" header, and if that's the case, then we:

- Current `unmacthed_request` flow:

1. Create a connection to the original destination (let's call this the `agent_original` connection);
2. Forward the HTTP request to it, awaiting its response;
3. Send the response back through our hyper connection (the `agent_client` connection we handle in hyper);

- The new steps:

4. Send the `agent_original` connection to the `filter_task` (through the `upgrade_tx` channel);
5. Retrieve the `agent_client` connection from hyper, as a normal `TcpStream`;
6. Copy whatever bytes were leftover from these connections (bytes that were read, but not processed by hyper);
7. Use [`copy_bidirectional`](https://docs.rs/tokio/latest/tokio/io/fn.copy_bidirectional.html) to keep copying from one connection to the other.

### Tests

The feature is being tested by `test_websocket_upgrade`, which is very similar to the `test_complete_passthrough` test. The main difference is that we use [`tokio-tugstenite`](https://docs.rs/tokio-tungstenite/latest/tokio_tungstenite/) to create a websocket connection (instead of a normal `TcpStream`).

## Alternatives

### Give a [`DuplexStream`](https://docs.rs/tokio/latest/tokio/io/struct.DuplexStream.html) to hyper

We could feed hyper a `DuplexStream` rather than the actual `TcpStream` connection, and deal with the upgrade ourselves, by parsing the bytes into an `Header`, checking for the "Upgrade: " header, or even use the [`Response`] as a way to identify that this was an upgrade request.

I've explored this approach, but this seemed a bit too much, especially when @t4lz asked me about "why not use the `unmatched_request`?"

### Use [`hyper::upgrade::on`]

hyper has a proper handler for dealing with HTTP upgrade requests, where you give it a [`Request`](https://docs.rs/hyper/1.0.0-rc.2/hyper/struct.Request.html) that contains an [`OnUpgrade`](https://docs.rs/hyper/1.0.0-rc.2/hyper/upgrade/struct.OnUpgrade.html) extension, and it'll give you back its connection (`agent_client`) and the leftover bytes.

I could not make this approach work, as we need to give the request to [`SendRequest`](https://docs.rs/hyper/1.0.0-rc.2/hyper/client/conn/http1/struct.SendRequest.html) by value, making it impossible to also feed it to the `upgrade::on` function. This was the case because we need to poll the connection (`agent_original`) in a separate task, thus requiring us to also move the request into it.

The request cannot be copied/cloned, as it contains a `Receiver` channel for the `OnUpgrade` feature, and if you try to manually copy it, the upgrade is never triggered, because we're dealing with different channels.

Co-authored-by: meowjesty <[email protected]>
bors bot pushed a commit that referenced this issue Jan 23, 2023
…led. (#973)

Issue: #928 

Depends on [test-images #14](metalbear-co/test-images#14).

Implements a pass-through mechanism to handle HTTP/1 upgrade requests from a client.

## Motivation

Currently, if you run mirrord with steal traffic, and HTTP filter enabled, we end up not serving HTTP upgrade requests appropriately. Our implementation of [`Service`](https://docs.rs/hyper/1.0.0-rc.2/hyper/service/trait.Service.html) would either:

- Forward the HTTP request to the local app (if a filter matched), send back a response and **not** upgrade the connection, or;
- Pass the request to its original destination, get a response back from it, and, again, **not** upgrade the connection.

This would prevent the user from handling websockets (our main motivator behind this feature), that start up as HTTP requests.

## Implementation

We're manually handling connection upgrades, by first checking if an HTTP request contains the "Upgrade" header, and if that's the case, then we:

- Current `unmacthed_request` flow:

1. Create a connection to the original destination (let's call this the `agent_original` connection);
2. Forward the HTTP request to it, awaiting its response;
3. Send the response back through our hyper connection (the `agent_client` connection we handle in hyper);

- The new steps:

4. Send the `agent_original` connection to the `filter_task` (through the `upgrade_tx` channel);
5. Retrieve the `agent_client` connection from hyper, as a normal `TcpStream`;
6. Copy whatever bytes were leftover from these connections (bytes that were read, but not processed by hyper);
7. Use [`copy_bidirectional`](https://docs.rs/tokio/latest/tokio/io/fn.copy_bidirectional.html) to keep copying from one connection to the other.

### Tests

The feature is being tested by `test_websocket_upgrade`, which is very similar to the `test_complete_passthrough` test. The main difference is that we use [`tokio-tugstenite`](https://docs.rs/tokio-tungstenite/latest/tokio_tungstenite/) to create a websocket connection (instead of a normal `TcpStream`).

## Alternatives

### Give a [`DuplexStream`](https://docs.rs/tokio/latest/tokio/io/struct.DuplexStream.html) to hyper

We could feed hyper a `DuplexStream` rather than the actual `TcpStream` connection, and deal with the upgrade ourselves, by parsing the bytes into an `Header`, checking for the "Upgrade: " header, or even use the [`Response`] as a way to identify that this was an upgrade request.

I've explored this approach, but this seemed a bit too much, especially when @t4lz asked me about "why not use the `unmatched_request`?"

### Use [`hyper::upgrade::on`]

hyper has a proper handler for dealing with HTTP upgrade requests, where you give it a [`Request`](https://docs.rs/hyper/1.0.0-rc.2/hyper/struct.Request.html) that contains an [`OnUpgrade`](https://docs.rs/hyper/1.0.0-rc.2/hyper/upgrade/struct.OnUpgrade.html) extension, and it'll give you back its connection (`agent_client`) and the leftover bytes.

I could not make this approach work, as we need to give the request to [`SendRequest`](https://docs.rs/hyper/1.0.0-rc.2/hyper/client/conn/http1/struct.SendRequest.html) by value, making it impossible to also feed it to the `upgrade::on` function. This was the case because we need to poll the connection (`agent_original`) in a separate task, thus requiring us to also move the request into it.

The request cannot be copied/cloned, as it contains a `Receiver` channel for the `OnUpgrade` feature, and if you try to manually copy it, the upgrade is never triggered, because we're dealing with different channels.

Co-authored-by: meowjesty <[email protected]>
@meowjesty
Copy link
Member Author

Should be working as of #973

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
1 participant