Skip to content

Commit

Permalink
Merge branch 'NET-1427-add-udp-rules' into 'master'
Browse files Browse the repository at this point in the history
feat(firewall): [NET-1427] Add UDP protocol to whitelisting of nodes.

Closes NET-1427

This MR adds a new firewall rule template for UDP traffic and a placeholder for `UDP` rules. Firewall rules from the registry remain unchanged, and will default to `TCP`. This is needed such that we can open specific ports for `UDP` connections for the new P2P transport over QUIC. 

Closes NET-1427

See merge request dfinity-lab/public/ic!13536
  • Loading branch information
DSharifi committed Jul 14, 2023
2 parents ae0e252 + e5cf644 commit 362d410
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 49 deletions.
15 changes: 10 additions & 5 deletions ic-os/guestos/rootfs/opt/ic/share/ic.json5.template
Expand Up @@ -172,7 +172,8 @@
icmp type parameter-problem accept\n\
icmp type echo-request accept\n\
icmp type echo-reply accept\n\
<<IPv4_RULES>>\n\
<<IPv4_TCP_RULES>>\n\
<<IPv4_UDP_RULES>>\n\
}\n\
\n\
chain FORWARD {\n\
Expand Down Expand Up @@ -218,7 +219,8 @@ table ip6 filter {\n\
icmpv6 type nd-router-advert accept\n\
icmpv6 type nd-neighbor-solicit accept\n\
icmpv6 type nd-neighbor-advert accept\n\
<<IPv6_RULES>>\n\
<<IPv6_TCP_RULES>>\n\
<<IPv6_UDP_RULES>>\n\
}\n\
\n\
chain FORWARD {\n\
Expand All @@ -232,8 +234,10 @@ table ip6 filter {\n\
<<IPv6_OUTBOUND_RULES>>\n\
}\n\
}\n",
ipv4_rule_template: "ip saddr {<<IPv4_PREFIXES>>} ct state { new } tcp dport {<<PORTS>>} <<ACTION>> # <<COMMENT>>",
ipv6_rule_template: "ip6 saddr {<<IPv6_PREFIXES>>} ct state { new } tcp dport {<<PORTS>>} <<ACTION>> # <<COMMENT>>",
ipv4_tcp_rule_template: "ip saddr {<<IPv4_PREFIXES>>} ct state { new } tcp dport {<<PORTS>>} <<ACTION>> # <<COMMENT>>",
ipv6_tcp_rule_template: "ip6 saddr {<<IPv6_PREFIXES>>} ct state { new } tcp dport {<<PORTS>>} <<ACTION>> # <<COMMENT>>",
ipv4_udp_rule_template: "ip saddr {<<IPv4_PREFIXES>>} udp dport {<<PORTS>>} <<ACTION>> # <<COMMENT>>",
ipv6_udp_rule_template: "ip6 saddr {<<IPv6_PREFIXES>>} udp dport {<<PORTS>>} <<ACTION>> # <<COMMENT>>",
ipv4_user_output_rule_template: "meta skuid <<USER>> ip daddr {<<IPv4_PREFIXES>>} ct state { new } tcp dport {<<PORTS>>} <<ACTION>> # <<COMMENT>>",
ipv6_user_output_rule_template: "meta skuid <<USER>> ip6 daddr {<<IPv6_PREFIXES>>} ct state { new } tcp dport {<<PORTS>>} <<ACTION>> # <<COMMENT>>",
default_rules: [{
Expand Down Expand Up @@ -316,7 +320,8 @@ table ip6 filter {\n\
comment: "Default rule from template",
direction: 1,
}],
ports_for_node_whitelist: [2497, 4100, 8080],
tcp_ports_for_node_whitelist: [2497, 4100, 8080],
udp_ports_for_node_whitelist: [4100],
ports_for_http_adapter_blacklist: [22, 2497, 4100, 7070, 8080, 9090, 9091, 9100, 19531],
max_simultaneous_connections_per_ip_address: 100,
},
Expand Down
9 changes: 6 additions & 3 deletions rs/config/src/config_sample.rs
Expand Up @@ -322,12 +322,15 @@ pub const SAMPLE_CONFIG: &str = r#"
firewall: {
config_file: "/path/to/nftables/config",
file_template: "",
ipv4_rule_template: "",
ipv6_rule_template: "",
ipv4_tcp_rule_template: "",
ipv4_udp_rule_template: "",
ipv6_tcp_rule_template: "",
ipv6_udp_rule_template: "",
ipv4_user_output_rule_template: "",
ipv6_user_output_rule_template: "",
default_rules: [],
ports_for_node_whitelist: [],
tcp_ports_for_node_whitelist: [],
udp_ports_for_node_whitelist: [],
ports_for_http_adapter_blacklist: [],
max_simultaneous_connections_per_ip_address: 0,
},
Expand Down
20 changes: 13 additions & 7 deletions rs/config/src/firewall.rs
Expand Up @@ -22,15 +22,18 @@ pub struct Config {
proptest(strategy = "any::<String>().prop_map(|x| PathBuf::from(x))")
)]
pub config_file: PathBuf,

pub file_template: String,
pub ipv4_rule_template: String,
pub ipv6_rule_template: String,
pub ipv4_tcp_rule_template: String,
pub ipv6_tcp_rule_template: String,
pub ipv4_udp_rule_template: String,
pub ipv6_udp_rule_template: String,
pub ipv4_user_output_rule_template: String,
pub ipv6_user_output_rule_template: String,
#[cfg_attr(test, proptest(strategy = "any::<String>().prop_map(|_x| vec![])"))]
pub default_rules: Vec<FirewallRule>,
pub ports_for_node_whitelist: Vec<u32>,
/// A map from protocol, UDP or TCP, to a list of ports that the node will use to whitelist for other nodes in the subnet.
pub tcp_ports_for_node_whitelist: Vec<u32>,
pub udp_ports_for_node_whitelist: Vec<u32>,
pub ports_for_http_adapter_blacklist: Vec<u32>,
/// We allow a maximum of `max_simultaneous_connections_per_ip_address` persistent connections to any ip address.
/// Any ip address with `max_simultaneous_connections_per_ip_address` connections will be dropped if a new connection is attempted.
Expand All @@ -42,12 +45,15 @@ impl Default for Config {
Self {
config_file: PathBuf::from(FIREWALL_FILE_DEFAULT_PATH),
file_template: "".to_string(),
ipv4_rule_template: "".to_string(),
ipv6_rule_template: "".to_string(),
ipv4_tcp_rule_template: "".to_string(),
ipv6_tcp_rule_template: "".to_string(),
ipv4_udp_rule_template: "".to_string(),
ipv6_udp_rule_template: "".to_string(),
ipv4_user_output_rule_template: "".to_string(),
ipv6_user_output_rule_template: "".to_string(),
default_rules: vec![],
ports_for_node_whitelist: vec![],
tcp_ports_for_node_whitelist: vec![],
udp_ports_for_node_whitelist: vec![],
ports_for_http_adapter_blacklist: vec![],
max_simultaneous_connections_per_ip_address: 0,
}
Expand Down
139 changes: 105 additions & 34 deletions rs/orchestrator/src/firewall.rs
Expand Up @@ -105,31 +105,33 @@ impl Firewall {

// This is the eventual list of rules fetched from the registry. It is build in the order of the priority:
// Node > Subnet > Replica Nodes > Global
let mut rules = Vec::<FirewallRule>::new();
let mut tcp_rules = Vec::<FirewallRule>::new();
let mut udp_rules = Vec::<FirewallRule>::new();

// First, we fetch the rules that are specific for this node
rules.append(
tcp_rules.append(
&mut self
.fetch_from_registry(registry_version, &FirewallRulesScope::Node(self.node_id)),
);

// Then we fetch the rules that are specific for the subnet, if one is assigned
if let Some(subnet_id) = subnet_id_opt {
rules.append(
tcp_rules.append(
&mut self
.fetch_from_registry(registry_version, &FirewallRulesScope::Subnet(subnet_id)),
);
}

// Then the rules that apply to all replica nodes
rules.append(
tcp_rules.append(
&mut self.fetch_from_registry(registry_version, &FirewallRulesScope::ReplicaNodes),
);

// Lastly, rules that apply globally to any type of node
rules.append(&mut self.fetch_from_registry(registry_version, &FirewallRulesScope::Global));
tcp_rules
.append(&mut self.fetch_from_registry(registry_version, &FirewallRulesScope::Global));

if !rules.is_empty() {
if !tcp_rules.is_empty() {
// We found some rules in the registry, so we will not use the default rules in the config file
self.source = DataSource::Registry;
} else {
Expand All @@ -140,7 +142,7 @@ impl Firewall {
"Firewall configuration was not found in registry. Using config file instead. This warning should be ignored when firewall config is not expected to appear in the registry (e.g., on testnets)."
);
self.source = DataSource::Config;
rules.append(&mut self.configuration.default_rules.clone());
tcp_rules.append(&mut self.configuration.default_rules.clone());
}

// Whitelisting for node IPs
Expand Down Expand Up @@ -172,18 +174,29 @@ impl Firewall {
);

// Build a single rule to whitelist all v4 and v6 IP addresses of nodes
let node_whitelisting_rule = FirewallRule {
let tcp_node_whitelisting_rule = FirewallRule {
ipv4_prefixes: node_ipv4s.clone(),
ipv6_prefixes: node_ipv6s.clone(),
ports: self.configuration.ports_for_node_whitelist.clone(),
ports: self.configuration.tcp_ports_for_node_whitelist.clone(),
action: FirewallAction::Allow as i32,
comment: "Automatic node whitelisting".to_string(),
user: None,
direction: Some(FirewallRuleDirection::Inbound as i32),
};

// Insert the whitelisting rule at the top of the list (highest priority)
rules.insert(0, node_whitelisting_rule);
let udp_node_whitelisting_rule = FirewallRule {
ipv4_prefixes: node_ipv4s.clone(),
ipv6_prefixes: node_ipv6s.clone(),
ports: self.configuration.udp_ports_for_node_whitelist.clone(),
action: FirewallAction::Allow as i32,
comment: "Automatic node whitelisting".to_string(),
user: None,
direction: Some(FirewallRuleDirection::Inbound as i32),
};

// Insert the whitelisting rules at the top of the list (highest priority)
tcp_rules.insert(0, tcp_node_whitelisting_rule);
udp_rules.insert(0, udp_node_whitelisting_rule);

// Blacklisting for Canister HTTP requests
// In addition to any explicit firewall rules we might apply, we also ALWAYS blacklist the ic-http-adapter used from accessing
Expand All @@ -203,10 +216,11 @@ impl Firewall {
};

// Insert the ic-http-adapter rule at the top of the list (highest priority)
rules.insert(0, ic_http_adapter_rule);
tcp_rules.insert(0, ic_http_adapter_rule);

// Generate the firewall file content
let content = Self::generate_firewall_file_content_full(&self.configuration, rules);
let content =
Self::generate_firewall_file_content_full(&self.configuration, tcp_rules, udp_rules);

let changed = content.ne(&self.compiled_config);
if changed {
Expand Down Expand Up @@ -255,26 +269,49 @@ impl Firewall {
/// Generates a string with the content for the firewall rules file
fn generate_firewall_file_content_full(
config: &FirewallConfig,
rules: Vec<FirewallRule>,
tcp_rules: Vec<FirewallRule>,
udp_rules: Vec<FirewallRule>,
) -> String {
config
.file_template
.replace(
"<<IPv4_RULES>>",
"<<IPv4_TCP_RULES>>",
&Self::compile_rules(
&config.ipv4_rule_template,
&rules,
&config.ipv4_tcp_rule_template,
&tcp_rules,
vec![
FirewallRuleDirection::Inbound,
FirewallRuleDirection::Unspecified,
],
),
)
.replace(
"<<IPv6_RULES>>",
"<<IPv4_UDP_RULES>>",
&Self::compile_rules(
&config.ipv6_rule_template,
&rules,
&config.ipv4_udp_rule_template,
&udp_rules,
vec![
FirewallRuleDirection::Inbound,
FirewallRuleDirection::Unspecified,
],
),
)
.replace(
"<<IPv6_TCP_RULES>>",
&Self::compile_rules(
&config.ipv6_tcp_rule_template,
&tcp_rules,
vec![
FirewallRuleDirection::Inbound,
FirewallRuleDirection::Unspecified,
],
),
)
.replace(
"<<IPv6_UDP_RULES>>",
&Self::compile_rules(
&config.ipv6_udp_rule_template,
&udp_rules,
vec![
FirewallRuleDirection::Inbound,
FirewallRuleDirection::Unspecified,
Expand All @@ -285,15 +322,15 @@ impl Firewall {
"<<IPv4_OUTBOUND_RULES>>",
&Self::compile_rules(
&config.ipv4_user_output_rule_template,
&rules,
&tcp_rules,
vec![FirewallRuleDirection::Outbound],
),
)
.replace(
"<<IPv6_OUTBOUND_RULES>>",
&Self::compile_rules(
&config.ipv6_user_output_rule_template,
&rules,
&tcp_rules,
vec![FirewallRuleDirection::Outbound],
),
)
Expand Down Expand Up @@ -420,11 +457,15 @@ fn test_firewall_rule_compilation() {
"<<IPv6_PREFIXES>>", "<<PORTS>>", "<<ACTION>>", "<<COMMENT>>"
);
let file_template = format!(
"{} {} {}",
"<<MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS>>", "<<IPv4_RULES>>", "<<IPv6_RULES>>"
"{} {} {} {} {}",
"<<MAX_SIMULTANEOUS_CONNECTIONS_PER_IP_ADDRESS>>",
"<<IPv4_TCP_RULES>>",
"<<IPv4_UDP_RULES>>",
"<<IPv6_TCP_RULES>>",
"<<IPv6_UDP_RULES>>",
);

let rules = vec![
let tcp_rules = vec![
FirewallRule {
ipv4_prefixes: vec!["test_ipv4_1".to_string()],
ipv6_prefixes: vec!["test_ipv6_1".to_string()],
Expand Down Expand Up @@ -463,36 +504,66 @@ fn test_firewall_rule_compilation() {
},
];

let expected_rules_compiled_v4 = vec![
let udp_rules = vec![FirewallRule {
ipv4_prefixes: vec!["test_ipv4_5_udp".to_string()],
ipv6_prefixes: vec!["test_ipv6_5_udp".to_string()],
ports: vec![13, 14, 15],
action: 1,
comment: "comment5".to_string(),
user: None,
direction: Some(FirewallRuleDirection::Inbound as i32),
}];

let expected_tcp_rules_compiled_v4 = vec![
format!("{} {} {} {}", "test_ipv4_1", "1,2,3", "accept", "comment1"),
format!("{} {} {} {}", "test_ipv4_2", "4,5,6", "drop", "comment2"),
];
let expected_rules_compiled_v6 = vec![

let expected_udp_rules_compiled_v4 = vec![format!(
"{} {} {} {}",
"test_ipv4_5_udp", "13,14,15", "accept", "comment5"
)];

let expected_tcp_rules_compiled_v6 = vec![
format!("{} {} {} {}", "test_ipv6_1", "1,2,3", "accept", "comment1"),
format!("{} {} {} {}", "test_ipv6_3", "7,8,9", "drop", "comment3"),
];

let expected_udp_rules_compiled_v6 = vec![format!(
"{} {} {} {}",
"test_ipv6_5_udp", "13,14,15", "accept", "comment5"
)];

let expected_file_content = format!(
"{} {} {}",
"{} {} {} {} {}",
max_simultaneous_connections_per_ip_address,
expected_rules_compiled_v4.join("\n"),
expected_rules_compiled_v6.join("\n")
expected_tcp_rules_compiled_v4.join("\n"),
expected_udp_rules_compiled_v4.join("\n"),
expected_tcp_rules_compiled_v6.join("\n"),
expected_udp_rules_compiled_v6.join("\n"),
);

let config = FirewallConfig {
config_file: PathBuf::default(),
file_template,
ipv4_rule_template,
ipv6_rule_template,

ipv4_tcp_rule_template: ipv4_rule_template.clone(),
ipv4_udp_rule_template: ipv4_rule_template,

ipv6_tcp_rule_template: ipv6_rule_template.clone(),
ipv6_udp_rule_template: ipv6_rule_template,

ipv4_user_output_rule_template: "".to_string(),
ipv6_user_output_rule_template: "".to_string(),
default_rules: vec![],
ports_for_node_whitelist: vec![],
tcp_ports_for_node_whitelist: vec![],
udp_ports_for_node_whitelist: vec![],
ports_for_http_adapter_blacklist: vec![],
max_simultaneous_connections_per_ip_address,
};

assert_eq!(
expected_file_content,
Firewall::generate_firewall_file_content_full(&config, rules)
Firewall::generate_firewall_file_content_full(&config, tcp_rules, udp_rules)
);
}

0 comments on commit 362d410

Please sign in to comment.