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

IPv6 support for traffic stealing #2976

Merged
merged 72 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
15359ac
E2E IPv6 steal test
t4lz Dec 3, 2024
af5db7c
e2e ipv6 service
t4lz Dec 4, 2024
d0c384b
Local E2E IPv6 testing
t4lz Dec 6, 2024
48094bd
No ephemeral, need to delete or uncomment later
t4lz Dec 9, 2024
6cec6b2
For local testing. DROP
t4lz Dec 10, 2024
f2343f2
add ipv6 flag
t4lz Dec 10, 2024
7442ecf
allow IPv6 in socket if enabled in config
t4lz Dec 10, 2024
2266f91
enable ipv6 in test config
t4lz Dec 10, 2024
8bd4e23
don't change CONTRIBUTING.md formatting
t4lz Dec 10, 2024
c733263
Use IpAddr instad of Ipv4Addr for pod IPs
t4lz Dec 10, 2024
c97dd1d
E2E test with portforwarding
t4lz Dec 12, 2024
ef96b64
fix tests import
t4lz Dec 13, 2024
7e06ba8
move ipv6 config up to network
t4lz Dec 13, 2024
e6da453
Propagate ipv6 setting to an agent arg
t4lz Dec 13, 2024
da569ba
fallback agent listener
t4lz Dec 13, 2024
fdcaced
stealer, iptables - start
t4lz Dec 17, 2024
19a5c38
add ipv6 listener and iptables, still need to adapt more places
t4lz Dec 20, 2024
1fdaf02
iptable listeners
t4lz Dec 22, 2024
6aff1c4
use filter table for ipv6
t4lz Dec 23, 2024
3e1d7ab
oh no
t4lz Dec 23, 2024
1222eee
Revert "oh no"
t4lz Dec 23, 2024
08d02c0
try with flush connections
t4lz Dec 23, 2024
2fc8138
use input chain for IPv6
t4lz Dec 24, 2024
c5e8568
fix dumb bug (ip6tables command switch)
t4lz Dec 30, 2024
8fba858
add debug logs
t4lz Dec 30, 2024
65236e6
add debug logs
t4lz Dec 30, 2024
5660ef6
revert some stuff
t4lz Dec 30, 2024
3350111
use nat table in ip6tables
t4lz Dec 30, 2024
1ce84ba
ipv6 manual test app
t4lz Dec 30, 2024
e730e4c
fix test request
t4lz Dec 30, 2024
1fcf661
fix doc?
t4lz Dec 31, 2024
a560193
thanks clippy
t4lz Dec 31, 2024
9f61628
ignore ipv6 test
t4lz Dec 31, 2024
5c80d23
fix config test
t4lz Dec 31, 2024
8d634f9
cfg test for ipv6 utils
t4lz Dec 31, 2024
e7144d7
easy way out
t4lz Dec 31, 2024
065a516
fix tests utils
t4lz Dec 31, 2024
227c1f0
ipv6 support default to false
t4lz Dec 31, 2024
06e432e
fix iptables tests
t4lz Dec 31, 2024
b11208d
remove unused methods
t4lz Dec 31, 2024
71c6d02
fix policies test
t4lz Dec 31, 2024
1470380
update schema
t4lz Dec 31, 2024
14d9853
run medschool
t4lz Dec 31, 2024
d756644
fix kube UT
t4lz Dec 31, 2024
eeba1f3
use test image agent
t4lz Jan 3, 2025
f87e7f3
changelog
t4lz Jan 3, 2025
22d3c02
use published test image again
t4lz Jan 3, 2025
ed0cd1c
TODOs
t4lz Jan 3, 2025
9d19851
add ipv6 test to CI
t4lz Jan 3, 2025
7f5e5e5
add kind cluster config for IPv6
t4lz Jan 3, 2025
14afc5b
fix cluster config
t4lz Jan 3, 2025
441ad71
CI IPv6 job name
t4lz Jan 3, 2025
5ddc69f
patch kind config to fix fail
t4lz Jan 3, 2025
5a68d7b
use kind bash script
t4lz Jan 3, 2025
21bbf48
fix cargo test command
t4lz Jan 3, 2025
dcdf89f
agent logs?
t4lz Jan 3, 2025
b409754
maybe with a longer TTL I'll get some logs?
t4lz Jan 7, 2025
42c04ed
print intproxy logs on failure
t4lz Jan 7, 2025
c2559c5
show nodes on failure
t4lz Jan 7, 2025
c119c88
modprobe?
t4lz Jan 7, 2025
59a8b3e
exec modprobe as command
t4lz Jan 7, 2025
6f618ac
which modprobe
t4lz Jan 7, 2025
c0ed134
docker file install kmod
t4lz Jan 7, 2025
ea87112
modprobe ip6_tables
t4lz Jan 7, 2025
953997d
load 3 modules
t4lz Jan 8, 2025
e5dca27
unused vars
t4lz Jan 8, 2025
8c60432
undo modprobes
t4lz Jan 8, 2025
d3db592
protocol cargo
t4lz Jan 8, 2025
07fd967
don't test ipv6 on CI
t4lz Jan 8, 2025
c49013c
delete kind cluster creation script, since not testing in CI
t4lz Jan 9, 2025
74c2d49
CR
t4lz Jan 9, 2025
1f5508b
apply change to new policy test
t4lz Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,25 @@ For example, a test which only tests sanity of the ephemeral container feature s

On Linux, running tests may exhaust a large amount of RAM and crash the machine. To prevent this, limit the number of concurrent jobs by running the command with e.g. `-j 4`

### IPv6

Some tests create a single-stack IPv6 service. They can only be run on clusters with IPv6 enabled.
In order to test IPv6 on a local cluster on macOS, you can use Kind:

1. `brew install kind`
2. ```shell
cat >kind-config.yaml <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
ipFamily: ipv6
apiServerAddress: 127.0.0.1
EOF
```
3. `kind create cluster --config kind-config.yaml`
4. When you run `kubectl get svc -o wide --all-namespaces` you should see IPv6 addresses.


### Cleanup

The Kubernetes resources created by the E2E tests are automatically deleted when the test exits. However, you can preserve resources from failed tests for debugging. To do this, set the `MIRRORD_E2E_PRESERVE_FAILED` variable to any value.
Expand Down
1 change: 1 addition & 0 deletions changelog.d/2956.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for stealing incoming connections that are over IPv6.
10 changes: 9 additions & 1 deletion mirrord-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1268,7 +1268,7 @@
},
"IncomingFileConfig": {
"title": "incoming (network)",
"description": "Controls the incoming TCP traffic feature.\n\nSee the incoming [reference](https://mirrord.dev/docs/reference/traffic/#incoming) for more details.\n\nIncoming traffic supports 2 modes of operation:\n\n1. Mirror (**default**): Sniffs the TCP data from a port, and forwards a copy to the interested listeners;\n\n2. Steal: Captures the TCP data from a port, and forwards it to the local process, see [`steal`](##steal);\n\n### Minimal `incoming` config\n\n```json { \"feature\": { \"network\": { \"incoming\": \"steal\" } } } ```\n\n### Advanced `incoming` config\n\n```json { \"feature\": { \"network\": { \"incoming\": { \"mode\": \"steal\", \"http_filter\": { \"header_filter\": \"host: api\\\\..+\" }, \"port_mapping\": [[ 7777, 8888 ]], \"ignore_localhost\": false, \"ignore_ports\": [9999, 10000] \"listen_ports\": [[80, 8111]] } } } } ```",
"description": "Controls the incoming TCP traffic feature.\n\nSee the incoming [reference](https://mirrord.dev/docs/reference/traffic/#incoming) for more details.\n\nIncoming traffic supports 2 modes of operation:\n\n1. Mirror (**default**): Sniffs the TCP data from a port, and forwards a copy to the interested listeners;\n\n2. Steal: Captures the TCP data from a port, and forwards it to the local process, see [`steal`](##steal);\n\n### Minimal `incoming` config\n\n```json { \"feature\": { \"network\": { \"incoming\": \"steal\" } } } ```\n\n### Advanced `incoming` config\n\n```json { \"feature\": { \"network\": { \"incoming\": { \"mode\": \"steal\", \"http_filter\": { \"header_filter\": \"host: api\\\\..+\" }, \"port_mapping\": [[ 7777, 8888 ]], \"ignore_localhost\": false, \"ignore_ports\": [9999, 10000], \"listen_ports\": [[80, 8111]] } } } } ```",
"anyOf": [
{
"anyOf": [
Expand Down Expand Up @@ -1474,6 +1474,14 @@
}
]
},
"ipv6": {
"title": "feature.network.ipv6 {#feature-network-dns}",
"description": "Enable ipv6 support. Turn on if your application listens to incoming traffic over IPv6.",
"type": [
"boolean",
"null"
]
},
"outgoing": {
"title": "feature.network.outgoing {#feature-network-outgoing}",
"anyOf": [
Expand Down
11 changes: 10 additions & 1 deletion mirrord/agent/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#![deny(missing_docs)]

use clap::{Parser, Subcommand};
use mirrord_protocol::{MeshVendor, AGENT_NETWORK_INTERFACE_ENV, AGENT_OPERATOR_CERT_ENV};
use mirrord_protocol::{
MeshVendor, AGENT_IPV6_ENV, AGENT_NETWORK_INTERFACE_ENV, AGENT_OPERATOR_CERT_ENV,
};

const DEFAULT_RUNTIME: &str = "containerd";

Expand Down Expand Up @@ -50,6 +52,13 @@ pub struct Args {
env = "MIRRORD_AGENT_IN_SERVICE_MESH"
)]
pub is_mesh: bool,

/// Enable support for IPv6-only clusters
///
/// Only when this option is set will take the needed steps to run on an IPv6 single stack
/// cluster.
#[arg(long, default_value_t = false, env = AGENT_IPV6_ENV)]
pub ipv6: bool,
}

impl Args {
Expand Down
44 changes: 34 additions & 10 deletions mirrord/agent/src/entrypoint.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{
collections::HashMap,
mem,
net::{Ipv4Addr, SocketAddrV4},
net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6},
path::PathBuf,
sync::{
atomic::{AtomicU32, Ordering},
Expand Down Expand Up @@ -492,11 +492,33 @@ impl ClientConnectionHandler {
async fn start_agent(args: Args) -> Result<()> {
trace!("start_agent -> Starting agent with args: {args:?}");

let listener = TcpListener::bind(SocketAddrV4::new(
// listen for client connections
let ipv4_listener_result = TcpListener::bind(SocketAddrV4::new(
Ipv4Addr::UNSPECIFIED,
args.communicate_port,
))
.await?;
.await;

let listener = if args.ipv6 && ipv4_listener_result.is_err() {
debug!("IPv6 Support enabled, and IPv4 bind failed, binding IPv6 listener");
TcpListener::bind(SocketAddrV6::new(
Ipv6Addr::UNSPECIFIED,
args.communicate_port,
0,
0,
))
.await
} else {
ipv4_listener_result
}?;

match listener.local_addr() {
Ok(addr) => debug!(
client_listener_address = addr.to_string(),
"Created listener."
),
Err(err) => error!(%err, "listener local address error"),
}

let state = State::new(&args).await?;

Expand Down Expand Up @@ -566,13 +588,15 @@ async fn start_agent(args: Args) -> Result<()> {
let cancellation_token = cancellation_token.clone();
let watched_task = WatchedTask::new(
TcpConnectionStealer::TASK_NAME,
TcpConnectionStealer::new(stealer_command_rx).and_then(|stealer| async move {
let res = stealer.start(cancellation_token).await;
if let Err(err) = res.as_ref() {
error!("Stealer failed: {err}");
}
res
}),
TcpConnectionStealer::new(stealer_command_rx, args.ipv6).and_then(
|stealer| async move {
let res = stealer.start(cancellation_token).await;
if let Err(err) = res.as_ref() {
error!("Stealer failed: {err}");
}
res
},
),
);
let status = watched_task.status();
let task = run_thread_in_namespace(
Expand Down
4 changes: 4 additions & 0 deletions mirrord/agent/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ pub(crate) enum AgentError {
/// Temporary error for vpn feature
#[error("Generic error in vpn: {0}")]
VpnError(String),

/// When we neither create a redirector for IPv4, nor for IPv6
#[error("Could not create a listener for stolen connections")]
CannotListenForStolenConnections,
}

impl From<mpsc::error::SendError<StealerCommand>> for AgentError {
Expand Down
26 changes: 21 additions & 5 deletions mirrord/agent/src/steal/connection.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
collections::{HashMap, HashSet},
net::{IpAddr, Ipv4Addr, SocketAddr},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
};

use fancy_regex::Regex;
Expand Down Expand Up @@ -289,6 +289,9 @@ pub(crate) struct TcpConnectionStealer {

/// Set of active connections stolen by [`Self::port_subscriptions`].
connections: StolenConnections,

/// Shen set, the stealer will use IPv6 if needed.
support_ipv6: bool,
}

impl TcpConnectionStealer {
Expand All @@ -297,14 +300,21 @@ impl TcpConnectionStealer {
/// Initializes a new [`TcpConnectionStealer`], but doesn't start the actual work.
/// You need to call [`TcpConnectionStealer::start`] to do so.
#[tracing::instrument(level = "trace")]
pub(crate) async fn new(command_rx: Receiver<StealerCommand>) -> Result<Self, AgentError> {
pub(crate) async fn new(
command_rx: Receiver<StealerCommand>,
support_ipv6: bool,
) -> Result<Self, AgentError> {
let config = envy::prefixed("MIRRORD_AGENT_")
.from_env::<TcpStealerConfig>()
.unwrap_or_default();

let port_subscriptions = {
let redirector =
IpTablesRedirector::new(config.stealer_flush_connections, config.pod_ips).await?;
let redirector = IpTablesRedirector::new(
config.stealer_flush_connections,
config.pod_ips,
support_ipv6,
)
.await?;

PortSubscriptions::new(redirector, 4)
};
Expand All @@ -315,6 +325,7 @@ impl TcpConnectionStealer {
clients: HashMap::with_capacity(8),
clients_closed: Default::default(),
connections: StolenConnections::with_capacity(8),
support_ipv6,
})
}

Expand Down Expand Up @@ -371,9 +382,14 @@ impl TcpConnectionStealer {
#[tracing::instrument(level = "trace", skip(self))]
async fn incoming_connection(&mut self, stream: TcpStream, peer: SocketAddr) -> Result<()> {
let mut real_address = orig_dst::orig_dst_addr(&stream)?;
let localhost = if self.support_ipv6 && real_address.is_ipv6() {
IpAddr::V6(Ipv6Addr::LOCALHOST)
} else {
IpAddr::V4(Ipv4Addr::LOCALHOST)
};
// If we use the original IP we would go through prerouting and hit a loop.
// localhost should always work.
real_address.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
real_address.set_ip(localhost);

let Some(port_subscription) = self.port_subscriptions.get(real_address.port()).cloned()
else {
Expand Down
22 changes: 18 additions & 4 deletions mirrord/agent/src/steal/ip_tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ pub fn new_iptables() -> iptables::IPTables {
.expect("IPTables initialization may not fail!")
}

/// wrapper around iptables::new that uses nft or legacy based on env
pub fn new_ip6tables() -> iptables::IPTables {
if let Ok(val) = std::env::var("MIRRORD_AGENT_NFTABLES")
&& val.to_lowercase() == "true"
{
iptables::new_with_cmd("/usr/sbin/ip6tables-nft")
} else {
iptables::new_with_cmd("/usr/sbin/ip6tables-legacy")
}
.expect("IPTables initialization may not fail!")
}

impl Debug for IPTablesWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IPTablesWrapper")
Expand Down Expand Up @@ -140,7 +152,7 @@ impl IPTables for IPTablesWrapper {
}
}

#[tracing::instrument(level = "trace")]
#[tracing::instrument(level = tracing::Level::TRACE, skip(self), ret, fields(table_name=%self.table_name))]
fn create_chain(&self, name: &str) -> Result<()> {
self.tables
.new_chain(self.table_name, name)
Expand Down Expand Up @@ -220,6 +232,7 @@ where
ipt: IPT,
flush_connections: bool,
pod_ips: Option<&str>,
ipv6: bool,
) -> Result<Self> {
let ipt = Arc::new(ipt);

Expand All @@ -231,6 +244,7 @@ where
_ => Redirects::Mesh(MeshRedirect::create(ipt.clone(), vendor, pod_ips)?),
}
} else {
tracing::trace!(ipv6 = ipv6, "creating standard redirect");
match StandardRedirect::create(ipt.clone(), pod_ips) {
Err(err) => {
warn!("Unable to create StandardRedirect chain: {err}");
Expand Down Expand Up @@ -280,7 +294,7 @@ where
/// Adds the redirect rule to iptables.
///
/// Used to redirect packets when mirrord incoming feature is set to `steal`.
#[tracing::instrument(level = "trace", skip(self))]
#[tracing::instrument(level = tracing::Level::DEBUG, skip(self))]
pub(super) async fn add_redirect(
&self,
redirected_port: Port,
Expand Down Expand Up @@ -408,7 +422,7 @@ mod tests {
.times(1)
.returning(|_| Ok(()));

let ipt = SafeIpTables::create(mock, false, None)
let ipt = SafeIpTables::create(mock, false, None, false)
.await
.expect("Create Failed");

Expand Down Expand Up @@ -541,7 +555,7 @@ mod tests {
.times(1)
.returning(|_| Ok(()));

let ipt = SafeIpTables::create(mock, false, None)
let ipt = SafeIpTables::create(mock, false, None, false)
.await
.expect("Create Failed");

Expand Down
5 changes: 4 additions & 1 deletion mirrord/agent/src/steal/ip_tables/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ where
{
const ENTRYPOINT: &'static str = "OUTPUT";

#[tracing::instrument(skip(ipt), level = tracing::Level::TRACE)]
pub fn create(ipt: Arc<IPT>, chain_name: String, pod_ips: Option<&str>) -> Result<Self> {
let managed = IPTableChain::create(ipt, chain_name)?;
let managed = IPTableChain::create(ipt, chain_name.clone()).inspect_err(
|e| tracing::error!(%e, "Could not create iptables chain \"{chain_name}\"."),
)?;

let exclude_source_ips = pod_ips
.map(|pod_ips| format!("! -s {pod_ips}"))
Expand Down
Loading
Loading