diff --git a/.env b/.env index 21d5dd570..ac0103f1b 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -TRIGGERED_REF=v5.15.10 +TRIGGERED_REF=test-orig-ip diff --git a/crates/telio-firewall/src/chain_helpers.rs b/crates/telio-firewall/src/chain_helpers.rs index 3bab81614..5c47640fc 100644 --- a/crates/telio-firewall/src/chain_helpers.rs +++ b/crates/telio-firewall/src/chain_helpers.rs @@ -15,8 +15,8 @@ use crate::libfirewall::{ LIBFW_CONTRACK_STATE_RELATED, LIBFW_DIRECTION_INBOUND, LIBFW_DIRECTION_OUTBOUND, LIBFW_FILTER_ASSOCIATED_DATA, LIBFW_FILTER_CONNTRACK_STATE, LIBFW_FILTER_DIRECTION, LIBFW_FILTER_DNS_QUERY_DOMAIN, LIBFW_FILTER_DST_NETWORK, LIBFW_FILTER_ICMP_TYPE, - LIBFW_FILTER_NEXT_LVL_PROTO, LIBFW_FILTER_SRC_NETWORK, LIBFW_FILTER_TCP_FLAGS, - LIBFW_ICMP_TYPE_DESTINATION_UNREACHABLE, LIBFW_ICMP_TYPE_ECHO_REPLY, + LIBFW_FILTER_NEXT_LVL_PROTO, LIBFW_FILTER_ORIG_SRC_IP, LIBFW_FILTER_SRC_NETWORK, + LIBFW_FILTER_TCP_FLAGS, LIBFW_ICMP_TYPE_DESTINATION_UNREACHABLE, LIBFW_ICMP_TYPE_ECHO_REPLY, LIBFW_ICMP_TYPE_ECHO_REQUEST, LIBFW_ICMP_TYPE_PARAMETER_PROBLEM, LIBFW_ICMP_TYPE_REDIRECT_MESSAGE, LIBFW_ICMP_TYPE_ROUTER_ADVERTISEMENT, LIBFW_ICMP_TYPE_ROUTER_SOLICITATION, LIBFW_ICMP_TYPE_TIMESTAMP, @@ -93,6 +93,7 @@ pub(crate) enum FilterData { TcpFlags(u8), IcmpType(IcmpType), DnsQueryDomain(Vec), + OrigSrcIp(IpAddr), } #[derive(Clone, Debug, PartialEq, Eq)] @@ -120,24 +121,28 @@ impl From<&[u8]> for LibfwAssociatedData { } } +fn libfw_ip_addr(ip: IpAddr) -> LibfwIpAddr { + let (ip_type, ip_data) = match ip { + IpAddr::V4(ipv4_addr) => ( + LIBFW_IP_TYPE_V4, + LibfwIpData { + ipv4_bytes: ipv4_addr.octets(), + }, + ), + IpAddr::V6(ipv6_addr) => ( + LIBFW_IP_TYPE_V6, + LibfwIpData { + ipv6_bytes: ipv6_addr.octets(), + }, + ), + }; + LibfwIpAddr { ip_type, ip_data } +} + impl From<&NetworkFilterData> for LibfwNetworkFilter { fn from(value: &NetworkFilterData) -> Self { - let (ip_type, ip_data) = match value.network.network() { - IpAddr::V4(ipv4_addr) => ( - LIBFW_IP_TYPE_V4, - LibfwIpData { - ipv4_bytes: ipv4_addr.octets(), - }, - ), - IpAddr::V6(ipv6_addr) => ( - LIBFW_IP_TYPE_V6, - LibfwIpData { - ipv6_bytes: ipv6_addr.octets(), - }, - ), - }; LibfwNetworkFilter { - network_addr: LibfwIpAddr { ip_type, ip_data }, + network_addr: libfw_ip_addr(value.network.network()), network_prefix: value.network.prefix_len(), port_range_start: value.port_range.0, port_range_end: value.port_range.1, @@ -266,6 +271,13 @@ impl From<&Filter> for (LibfwFilter, Option) { Some(FilterExtraData::DomainSet(pinned)), ) } + FilterData::OrigSrcIp(ip) => ( + LibfwFilterData { + orig_src_ip: libfw_ip_addr(*ip), + }, + LIBFW_FILTER_ORIG_SRC_IP, + None, + ), }; ( LibfwFilter { @@ -380,8 +392,9 @@ pub mod tests { LIBFW_CONTRACK_STATE_ESTABLISHED, LIBFW_DIRECTION_INBOUND, LIBFW_DIRECTION_OUTBOUND, LIBFW_FILTER_ASSOCIATED_DATA, LIBFW_FILTER_CONNTRACK_STATE, LIBFW_FILTER_DIRECTION, LIBFW_FILTER_DNS_QUERY_DOMAIN, LIBFW_FILTER_DST_NETWORK, LIBFW_FILTER_ICMP_TYPE, - LIBFW_FILTER_NEXT_LVL_PROTO, LIBFW_FILTER_SRC_NETWORK, LIBFW_FILTER_TCP_FLAGS, - LIBFW_ICMP_TYPE_ECHO_REPLY, LIBFW_IP_TYPE_V4, LIBFW_IP_TYPE_V6, LIBFW_NEXT_PROTO_UDP, + LIBFW_FILTER_NEXT_LVL_PROTO, LIBFW_FILTER_ORIG_SRC_IP, LIBFW_FILTER_SRC_NETWORK, + LIBFW_FILTER_TCP_FLAGS, LIBFW_ICMP_TYPE_ECHO_REPLY, LIBFW_IP_TYPE_V4, LIBFW_IP_TYPE_V6, + LIBFW_NEXT_PROTO_UDP, }, }; @@ -429,6 +442,12 @@ pub mod tests { filter_data: FilterData::IcmpType(IcmpType::EchoReply), inverted: true, }, + Filter { + filter_data: FilterData::OrigSrcIp(std::net::IpAddr::V4(Ipv4Addr::new( + 10, 0, 0, 5, + ))), + inverted: false, + }, ], action: RuleAction::Drop, }, @@ -540,6 +559,18 @@ pub mod tests { icmp_type: LIBFW_ICMP_TYPE_ECHO_REPLY, }, }, + LibfwFilter { + inverted: false, + filter_type: LIBFW_FILTER_ORIG_SRC_IP, + filter: LibfwFilterData { + orig_src_ip: LibfwIpAddr { + ip_type: LIBFW_IP_TYPE_V4, + ip_data: LibfwIpData { + ipv4_bytes: [10, 0, 0, 5], + }, + }, + }, + }, ]; let assoc_data = vec![1u8; 32]; @@ -710,6 +741,20 @@ pub mod tests { }); } } + LIBFW_FILTER_ORIG_SRC_IP => { + let c = unsafe { conv.filter.orig_src_ip }; + let e = unsafe { expected.filter.orig_src_ip }; + assert_eq!(c.ip_type, e.ip_type); + match c.ip_type { + LIBFW_IP_TYPE_V4 => assert_eq!(unsafe { c.ip_data.ipv4_bytes }, unsafe { + e.ip_data.ipv4_bytes + }), + LIBFW_IP_TYPE_V6 => assert_eq!(unsafe { c.ip_data.ipv6_bytes }, unsafe { + e.ip_data.ipv6_bytes + }), + _ => unreachable!("Unknown IP type"), + }; + } _ => unreachable!("Unexpected filter type"), } } diff --git a/crates/telio-firewall/src/firewall.rs b/crates/telio-firewall/src/firewall.rs index 1a2cb50b2..308bb091a 100644 --- a/crates/telio-firewall/src/firewall.rs +++ b/crates/telio-firewall/src/firewall.rs @@ -32,8 +32,8 @@ use telio_utils::{ use crate::{ chain_helpers::{ - ConnectionState, Direction, FfiChainGuard, Filter, FilterData, NetworkFilterData, - NextLevelProtocol, Rule, RuleAction, + Direction, FfiChainGuard, Filter, FilterData, NetworkFilterData, NextLevelProtocol, Rule, + RuleAction, }, libfirewall::{LibfwChainV2, LibfwFirewall, LibfwLogLevel, LibfwResult, LibfwVerdict}, tp_lite_stats::{collect_stats, CallbackManager}, @@ -658,27 +658,12 @@ pub(crate) fn build_chain_rules( }); } - // Accept packets for locally initiated connections + // Accept incoming packets only for connections initiated from this IP rules.push(Rule { - filters: vec![ - Filter { - filter_data: FilterData::ConntrackState(ConnectionState::Established), - inverted: false, - }, - filter_dst_ip_all_ports(IpNet::from(*ip), false), - ], - action: RuleAction::Accept, - }); - - // And packets related to them - rules.push(Rule { - filters: vec![ - Filter { - filter_data: FilterData::ConntrackState(ConnectionState::Related), - inverted: false, - }, - filter_dst_ip_all_ports(IpNet::from(*ip), false), - ], + filters: vec![Filter { + filter_data: FilterData::OrigSrcIp(*ip), + inverted: false, + }], action: RuleAction::Accept, }); diff --git a/crates/telio-firewall/src/libfirewall.rs b/crates/telio-firewall/src/libfirewall.rs index 147aeb3ed..12b6f7a5d 100644 --- a/crates/telio-firewall/src/libfirewall.rs +++ b/crates/telio-firewall/src/libfirewall.rs @@ -45,6 +45,7 @@ pub const LIBFW_FILTER_NEXT_LVL_PROTO: u8 = 5; pub const LIBFW_FILTER_TCP_FLAGS: u8 = 6; pub const LIBFW_FILTER_ICMP_TYPE: u8 = 7; pub const LIBFW_FILTER_DNS_QUERY_DOMAIN: u8 = 8; +pub const LIBFW_FILTER_ORIG_SRC_IP: u8 = 9; #[repr(u32)] #[doc = " Log levels used in LibfwLogCallback\n"] #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] @@ -182,6 +183,8 @@ pub union LibfwFilterData { pub icmp_type: u8, #[doc = " Set of DNS domain patterns to match against DNS query QNAMEs\n Use when filter_type = LibfwFilterDnsQueryDomain"] pub dns_domain_set: LibfwDnsDomainSet, + #[doc = " Source IP of the connection's original (initiating) direction, taken from conntrack\n Use when filter_type = LIBFW_FILTER_ORIG_SRC_IP"] + pub orig_src_ip: LibfwIpAddr, } #[doc = " Struct describing a single Libfw filter\n"] #[repr(C)] diff --git a/crates/telio-firewall/tests/firewall_integration_tests.rs b/crates/telio-firewall/tests/firewall_integration_tests.rs index 33d255055..a905abfac 100644 --- a/crates/telio-firewall/tests/firewall_integration_tests.rs +++ b/crates/telio-firewall/tests/firewall_integration_tests.rs @@ -25,6 +25,7 @@ use telio_model::{ type MakeUdp = &'static dyn Fn(&str, &str) -> Vec; type MakeTcp = &'static dyn Fn(&str, &str, u8) -> Vec; type MakeIcmp = &'static dyn Fn(&str, &str, GenericIcmpType) -> Vec; +type MakeIcmpEcho = &'static dyn Fn(&str, &str, GenericIcmpType, u16) -> Vec; const IPV4_HEADER_MIN: usize = 20; // IPv4 header minimal length in bytes const IPV6_HEADER_MIN: usize = 40; // IPv6 header minimal length in bytes @@ -319,6 +320,59 @@ fn make_icmp6(src: &str, dst: &str, icmp_type: GenericIcmpType) -> Vec { make_icmp6_with_body(src, dst, icmp_type, &[]) } +// Builds a well-formed ICMP echo (request/reply) packet: the echo identifier is +// written to the first two bytes of the ICMP body and a valid checksum is +// computed. Both are required for libfirewall conntrack to track the connection +// (it keys ICMP flows on the identifier and rejects packets with a bad checksum). +// Request and reply that share an identifier are matched into the same flow. +fn make_icmp4_echo(src: &str, dst: &str, icmp_type: GenericIcmpType, identifier: u16) -> Vec { + let ip_len = IPV4_HEADER_MIN + ICMP_HEADER; + let mut raw = vec![0u8; ip_len]; + { + let mut packet = + MutableIcmpPacket::new(&mut raw[IPV4_HEADER_MIN..]).expect("ICMP: Bad ICMP buffer"); + packet.set_icmp_type(icmp_type.v4()); + packet.payload_mut()[0..2].copy_from_slice(&identifier.to_ne_bytes()); + let checksum = pnet_packet::icmp::checksum(&packet.to_immutable()); + packet.set_checksum(checksum); + } + let mut ip = MutableIpv4Packet::new(&mut raw).expect("ICMP: Bad IP buffer"); + set_ipv4( + &mut ip, + IpNextHeaderProtocols::Icmp, + IPV4_HEADER_MIN, + ip_len, + ); + ip.set_source(src.parse().expect("ICMP: Bad src IP")); + ip.set_destination(dst.parse().expect("ICMP: Bad dst IP")); + raw +} + +fn make_icmp6_echo(src: &str, dst: &str, icmp_type: GenericIcmpType, identifier: u16) -> Vec { + let ip_len = IPV6_HEADER_MIN + ICMP_HEADER; + let mut raw = vec![0u8; ip_len]; + let src_ip: Ipv6Addr = src.parse().expect("ICMP: Bad src IP"); + let dst_ip: Ipv6Addr = dst.parse().expect("ICMP: Bad dst IP"); + { + let mut packet = + MutableIcmpv6Packet::new(&mut raw[IPV6_HEADER_MIN..]).expect("ICMP: Bad ICMP buffer"); + packet.set_icmpv6_type(icmp_type.v6()); + packet.payload_mut()[0..2].copy_from_slice(&identifier.to_ne_bytes()); + let checksum = pnet_packet::icmpv6::checksum(&packet.to_immutable(), &src_ip, &dst_ip); + packet.set_checksum(checksum); + } + let mut ip = MutableIpv6Packet::new(&mut raw).expect("ICMP: Bad IP buffer"); + set_ipv6( + &mut ip, + IpNextHeaderProtocols::Icmpv6, + IPV6_HEADER_MIN, + ip_len, + ); + ip.set_source(src_ip); + ip.set_destination(dst_ip); + raw +} + const DNS_HEADER_SIZE: usize = 12; // Strings in the DNS message format are encoded as a list of labels followed by a null byte. @@ -533,11 +587,11 @@ fn ipv6_blocked() { assert_eq!(fw.process_outbound_packet_sink(&make_peer(), &mut make_udp(src4, dst1)), is_ipv4); assert_eq!(fw.process_outbound_packet_sink(&make_peer(), &mut make_udp(src1, dst1)), is_ipv4); - // Should PASS (matching outgoing connections exist in LRUCache) - assert_eq!(fw.process_inbound_packet(&make_peer(), &mut make_udp(dst1, src3)), is_ipv4); - assert_eq!(fw.process_inbound_packet(&make_peer(), &mut make_udp(dst1, src4)), is_ipv4); - assert_eq!(fw.process_inbound_packet(&make_peer(), &mut make_udp(dst1, src1)), is_ipv4); - assert_eq!(fw.process_inbound_packet(&make_peer(), &mut make_udp(dst1, src2)), is_ipv4); + // Dropped: peer-initiated flows (peer's packets seen first), not host-initiated + assert_eq!(fw.process_inbound_packet(&make_peer(), &mut make_udp(dst1, src3)), false); + assert_eq!(fw.process_inbound_packet(&make_peer(), &mut make_udp(dst1, src4)), false); + assert_eq!(fw.process_inbound_packet(&make_peer(), &mut make_udp(dst1, src1)), false); + assert_eq!(fw.process_inbound_packet(&make_peer(), &mut make_udp(dst1, src2)), false); // Should FAIL (has no matching outgoing connection) assert_eq!(fw.process_inbound_packet(&make_peer(), &mut make_udp(dst1, src5)), false); @@ -724,7 +778,8 @@ fn firewall_vpn_peer() { fw.apply_state(state.clone()); assert_eq!(fw.process_inbound_packet(&peer1.0, &mut make_udp(src1, dst1,)), false); assert_eq!(fw.process_inbound_packet(&peer2.0, &mut make_udp(src2, dst1,)), false); - assert_eq!(fw.process_inbound_packet(&peer1.0, &mut make_tcp(src1, dst1, TcpFlags::ACK)), true); + // Peer-initiated established TCP: no longer accepted once the vpn-peer rule is gone + assert_eq!(fw.process_inbound_packet(&peer1.0, &mut make_tcp(src1, dst1, TcpFlags::ACK)), false); assert_eq!(fw.process_inbound_packet(&peer2.0, &mut make_tcp(src2, dst1, TcpFlags::ACK)), false); } } @@ -776,6 +831,115 @@ fn firewall_whitelist_change_icmp() { } } +#[rustfmt::skip] +#[test] +fn firewall_orig_src_ip_peer_initiated_blocked_after_removal() { + // A peer that is whitelisted for incoming connections pings the host, and the + // host answers with ICMP echo replies. While the peer is whitelisted, the + // pings are accepted and a conntrack entry is opened whose *original* + // direction source is the peer's IP (the peer initiated the connection). + // + // Once the peer is removed from the incoming whitelist, further pings must be + // dropped: the incoming-peer rule is gone, and the OrigSrcIp rule only accepts + // packets for connections initiated *from the host's own IP*. Here the + // connection was initiated by the peer, so OrigSrcIp does not match and the + // (still ESTABLISHED) conntrack state is not enough to let the pings through. + struct TestInput { + us: &'static str, + them: &'static str, + make_icmp_echo: MakeIcmpEcho, + } + + // Echo identifier tying the request and its reply into the same conntrack flow. + const ID: u16 = 0x4242; + + let test_inputs = vec![ + TestInput{ us: "127.0.0.1", them: "8.8.8.8", make_icmp_echo: &make_icmp4_echo }, + TestInput{ us: "::1", them: "2001:4860:4860::8888", make_icmp_echo: &make_icmp6_echo }, + ]; + + for TestInput { us, them, make_icmp_echo } in &test_inputs { + let fw = StatefulFirewall::new(true, FeatureFirewall::default()).expect("Failed to load libfirewall"); + fw.apply_state(FirewallState { + ip_addresses: vec![IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))], + ..Default::default() + }); + + let mut state = fw.get_state(); + let peer = make_random_peer(); + + // Not whitelisted yet: the peer's pings are dropped. + assert_eq!(fw.process_inbound_packet(&peer.0, &mut make_icmp_echo(them, us, IcmpTypes::EchoRequest.into(), ID)), false); + + // Allow the peer to open incoming connections. + state.whitelist.peer_whitelists[Permissions::IncomingConnections].insert(peer); + fw.apply_state(state.clone()); + + // The peer pings the host -> accepted by the incoming-peer rule. This opens + // a conntrack entry whose original direction source is the peer's IP. + assert_eq!(fw.process_inbound_packet(&peer.0, &mut make_icmp_echo(them, us, IcmpTypes::EchoRequest.into(), ID)), true); + // The host answers with an echo reply -> accepted outbound; the flow is now ESTABLISHED. + assert_eq!(fw.process_outbound_packet_sink(&peer.0, &mut make_icmp_echo(us, them, IcmpTypes::EchoReply.into(), ID)), true); + // The connection is established, so subsequent pings keep flowing. + assert_eq!(fw.process_inbound_packet(&peer.0, &mut make_icmp_echo(them, us, IcmpTypes::EchoRequest.into(), ID)), true); + + // Remove the peer from the incoming whitelist. + state.whitelist.peer_whitelists[Permissions::IncomingConnections].remove(&peer); + fw.apply_state(state.clone()); + + // Pings must now fail: not from an allowed peer anymore, and the connection + // was initiated by the peer (orig src != our IP) so OrigSrcIp doesn't help - + // the ESTABLISHED state alone no longer accepts the traffic. + assert_eq!(fw.process_inbound_packet(&peer.0, &mut make_icmp_echo(them, us, IcmpTypes::EchoRequest.into(), ID)), false); + assert_eq!(fw.process_inbound_packet(&peer.0, &mut make_icmp_echo(them, us, IcmpTypes::EchoReply.into(), ID)), false); + } +} + +#[rustfmt::skip] +#[test] +fn firewall_orig_src_ip_host_initiated_allowed_without_whitelist() { + // Positive control for the OrigSrcIp rule: when the *host* initiates the + // connection (host pings the peer), the peer's replies must be accepted even + // though the peer is not on any whitelist, because the connection's original + // direction source is one of the host's own IPs. This proves the failure in + // `firewall_orig_src_ip_peer_initiated_blocked_after_removal` is specific to + // peer-initiated connections rather than a blanket drop. + struct TestInput { + us: &'static str, + them: &'static str, + make_icmp_echo: MakeIcmpEcho, + } + + // Echo identifier tying the request and its reply into the same conntrack flow. + const ID: u16 = 0x1337; + + let test_inputs = vec![ + TestInput{ us: "127.0.0.1", them: "8.8.8.8", make_icmp_echo: &make_icmp4_echo }, + TestInput{ us: "::1", them: "2001:4860:4860::8888", make_icmp_echo: &make_icmp6_echo }, + ]; + + for TestInput { us, them, make_icmp_echo } in &test_inputs { + let fw = StatefulFirewall::new(true, FeatureFirewall::default()).expect("Failed to load libfirewall"); + fw.apply_state(FirewallState { + ip_addresses: vec![IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))], + ..Default::default() + }); + + let peer = make_random_peer(); + + // The peer is not whitelisted, so unsolicited inbound pings are dropped. + assert_eq!(fw.process_inbound_packet(&peer.0, &mut make_icmp_echo(them, us, IcmpTypes::EchoRequest.into(), ID)), false); + + // The host pings the peer -> accepted outbound, opening a conntrack entry + // whose original direction source is the host's own IP. + assert_eq!(fw.process_outbound_packet_sink(&peer.0, &mut make_icmp_echo(us, them, IcmpTypes::EchoRequest.into(), ID)), true); + + // The peer's echo reply is accepted via the OrigSrcIp rule (orig src == us) + // even though the peer is on no whitelist. + assert_eq!(fw.process_inbound_packet(&peer.0, &mut make_icmp_echo(them, us, IcmpTypes::EchoReply.into(), ID)), true); + } +} + #[rustfmt::skip] #[test] fn firewall_whitelist_change_udp_allow() { @@ -799,13 +963,12 @@ fn firewall_whitelist_change_udp_allow() { state.whitelist.port_whitelist.insert(them_peer, 8888); fw.apply_state(state.clone()); // NOTE: this doesn't change anything about this test assert_eq!(fw.process_outbound_packet_sink(&them_peer.0, &mut make_udp(us, them)), true); - assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_udp(them, us)), true); - - // Should PASS because we started the session - assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_udp(them, us)), true); + // Peer-initiated (the peer's packet above was seen first), so OrigSrcIp doesn't accept it + assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_udp(them, us)), false); + assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_udp(them, us)), false); state.whitelist.port_whitelist.remove(&them_peer); fw.apply_state(state.clone()); // NOTE: also has no impact on this test - assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_udp(them, us)), true); + assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_udp(them, us)), false); } } @@ -836,7 +999,8 @@ fn firewall_whitelist_change_udp_block() { assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_udp(them, us)), true); state.whitelist.port_whitelist.remove(&them_peer); fw.apply_state(state.clone()); - assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_udp(them, us)), true); + // Peer-initiated: dropped after the port whitelist is removed (OrigSrcIp doesn't match) + assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_udp(them, us)), false); } } @@ -895,11 +1059,12 @@ fn firewall_whitelist_change_tcp_block() { assert_eq!(fw.process_outbound_packet_sink(&them_peer.0, &mut make_tcp(us, them, TcpFlags::SYN | TcpFlags::ACK)), true); - // Firewall allows already established connections + // Firewall allows already established connections while the peer is whitelisted assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_tcp(them, us, TcpFlags::ACK)), true); state.whitelist.port_whitelist.remove(&them_peer); fw.apply_state(state.clone()); - assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_tcp(them, us, TcpFlags::ACK)), true); + // Peer-initiated: dropped after the port whitelist is removed (OrigSrcIp doesn't match) + assert_eq!(fw.process_inbound_packet(&them_peer.0, &mut make_tcp(them, us, TcpFlags::ACK)), false); } }