Automatically manage firewall rules (iptables) for services #1
Loading…
Reference in a new issue
No description provided.
Delete branch "add-firewall-rules"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
We chose to use the iptables crate rather than the nftables one (nftnl) because it is much higher level and seems to be more mature/popular.
This adds a new firewall (fw) actor that handles event and a fw module that provides high level functions (setup, add port, cleanup).
Still TODO / needs to be discussed :
Should we cleanup? We should avoid state if possible
So in my review I propose a bit agressive solutions: flushing entries on each notification. Of course, we should be careful to not break the connection if we do that, it is maybe a bad idea. But we can cleanup at process start. Cleaning on stopping is a bit hard as process may crash, I think it will be lot of burden for close to no gain
IGD ports are removed by reaching a timeout due to not renewing them
Considering IGD, ports are registered with a timeout: if registration is not renewed, the entry is garbage collected automatically by the router.
On each notification, the full sets of ports to be opened is sent, so by not renewing ports that are not required, the NAT entry on the router will naturally expire.
Nice job, some minor comments before the merge to make your code more homogenous with our coding style ;)
@ -20,0 +24,4 @@
[...]
options {
docker.privileged.enabled = "true"
Uuuuh nicely spot, I did not think to it but it makes sense :)
Thanks for testing the whole pipeline!
@ -0,0 +2,4 @@
use regex::Regex;
use std::collections::HashSet;
use crate::messages;
Could you add the anyhow crate to handle errors please:
@ -0,0 +12,4 @@
}
}
pub fn setup(ipt: &iptables::IPTables) -> Result<(), FirewallError> {
anyhow will override your Result<> object, taking only a return value, error will be generic then.
eg:
This tip applies for the whole document, and in any case build will fail as soon as you will have added the anyhow use statement.
@ -0,0 +14,4 @@
pub fn setup(ipt: &iptables::IPTables) -> Result<(), FirewallError> {
if !ipt.chain_exists("filter", "DIPLONAT")? {
It's very clever to put the rules in a separate chain, well done ;)
@ -0,0 +23,4 @@
Ok(())
}
pub fn open_ports(ipt: &iptables::IPTables, ports: messages::PublicExposedPorts) -> Result<(), FirewallError> {
Same as before, you can just use
Result<()>
@ -0,0 +35,4 @@
Ok(())
}
pub fn get_opened_ports(ipt: &iptables::IPTables) -> Result<messages::PublicExposedPorts, FirewallError> {
Same as before, you can just use
Result<messages::PublicExposedPorts>
@ -0,0 +42,4 @@
};
let list = ipt.list("filter", "DIPLONAT")?;
let re = Regex::new(r"-A.*? -p (w+).*--dport (d+).*?-j ACCEPT").unwrap();
A convention we chose with LX (we should document it, mea culpa) is to use the anyhow crate and to handle errors without crashing whenever possible but forwarding it "higher" in the call stack.
The idea is the caller can choose to crash or to bypass the error and then your code can be used as a handful library. So, as your return type is already a
Result
, you can returnOk()
or, thanks to anyhow, any kind of error. You can simply rewrite your statement as follow (as soon as you have added anyhow):To help debugging, you can add a context to your error:
As a more general rule, do not use:
expect
unwrap
Instead use:
?
context
@ -0,0 +47,4 @@
let caps = re.captures(&i);
match caps {
Some(c) => {
let raw_proto = c.get(1).unwrap();
Lines 50, 51 and 54 you can replace
unwrap
by?
@ -0,0 +67,4 @@
Ok(ports)
}
pub fn cleanup(ipt: &iptables::IPTables) -> Result<(), FirewallError> {
Same as before, you can just use
Result<()>
Moreover, I see that you never call this function.
What about calling it on the setup, so if the app reboots at least it will start with a clean state?
Even more agressive, could we not just simply regenerates the whole chain on every change?
So, no difference to compute or ports to close?
I believe that on each modification to iptables, you have to flush the whole set of rules,
so if we could do a single "commit" to the kernel with a brand new chain it could be great.
In practise:
@ -0,0 +23,4 @@
impl FirewallActor {
pub async fn new(_refresh: Duration, rxp: &watch::Receiver<messages::PublicExposedPorts>) -> Result<Self> {
let ctx = Self {
ipt: iptables::new(false).unwrap(),
Replace with
?
@ -0,0 +29,4 @@
refresh: _refresh,
};
fw::setup(&ctx.ipt).expect("iptables setup failed");
Here also you can put a
?
:)@ -0,0 +50,4 @@
// 3. Update firewall rules
match self.do_fw_update().await {
Ok(()) => debug!("Successfully updated firewall rules"),
Err(e) => error!("An error occured while updating firewall rules. {}", e),
By putting
?
everywhere, we can handle all errors here.Currently, this line can't be called as there is no way that
do_fw_update
returns anErr
as inside the function, you never returns anErr
asunwrap
makes the app panic ;)@ -0,0 +56,4 @@
}
pub async fn do_fw_update(&self) -> Result<()> {
let curr_opened_ports = fw::get_opened_ports(&self.ipt).unwrap();
You should replace
unwrap
;)@ -0,0 +58,4 @@
pub async fn do_fw_update(&self) -> Result<()> {
let curr_opened_ports = fw::get_opened_ports(&self.ipt).unwrap();
let diff_tcp = self.last_ports.tcp_ports.difference(&curr_opened_ports.tcp_ports).copied().collect::<HashSet<u16>>();
Nice idea the set difference ;)
Thanks a lot :)
@ -0,0 +65,4 @@
tcp_ports: diff_tcp,
udp_ports: diff_udp
};
I see, maybe we could add later logic to close ports but I also like the idea to do the things incrementally.
So let's focus on stabilizing the current features and see how it runs in production :)
@ -0,0 +66,4 @@
udp_ports: diff_udp
};
fw::open_ports(&self.ipt, ports_to_open).unwrap();
Here also ;)
I'm a little concerned about flushing entries on notification for the exact reason you are mentioning. I'd rather test it in another version as you are suggesting :)
Agreed. Cleaning up on start is much more reliable than cleaning up on stop. I added that to the code.