Library to create firewall rules


#1

Hello! I’m new to rust and I’m tasked with creating a file that does the following

  • creates firewall rules.

I’ve gotten the following code working to do this.

 make_firewall_tcp(r#"name="anewrule""#, "dir=in", "action=allow", "protocol=TCP", "localport=19800-19820");

fn make_firewall_tcp(name:&str, dir:&str, action:&str, protocol:&str, localport:&str){
let _child = Command::new("netsh")
 .arg("advfirewall")
 .arg("firewall")
 .arg("add")
 .arg("rule")
 .arg(name)
 .arg(dir)
 .arg(action)
 .arg(protocol)
 .arg(localport)
       .spawn().expect("failed");
    thread::sleep(time::Duration::new(2, 0)); // Windows needs time!   
   println!("done");
   let _output = _child.stdout;
println!("{:?}", _output);

}

I was just wondering if anyone could point out any good rust libraries that does this, maybe more elegantly?

Also any other rust library pointers would be awesome!
I also need to do the following with rust:
-check the os version.

  • install programs in certain places
  • check .net version

#2

Hi @heplyler!

Some pointers:

  • Instead of executing commands, you may want to use COM (that’s the only API I found that works with Windows Firewall), There is a cargo that wraps COM called com-rs - I don’t know if it works, it might be worth checking.
  • If you’d like to stick to executing commands, I would check the status instead of panicing + avoid sleeping:
let child = Command::new("netsh")
    .arg("advfirewall")
    // some more arguments...
    .spawn()
    .status();

match child {
    Ok(exit_code) => {
        // check return code, report command failure if non-zero...
        println!("netsh returned {}", exit_code.code());
    },
    Err(err) => {
        // handle the error.
        eprintln!("failed to execute: {}", err);
    },
}
  • You may want to use kernel32-sys for other WinAPI related tasks, such as checking OS version
  • The winreg crate would be helpful to check installed programs & .NET versions.
  • Instead of writing “name=”, “dir=”, etc… It would be nicer if the make_firewall_tcp would handle that, Some of those options can be enums (that will avoid future problems)
    Overall, a firewall can be complex - consider implementing a Struct that can add/modify/delete firewall rules.

#3

Thank you! I will check those out and try to implement them today.
:sunglasses:


#4

Okay, so far I now have this code, which looks alot better. I’m not really familiar with COM and so working with command prompt seems the safer bet, so I’ve created this bit of code:

 use std::process::Command;


fn main() {
    command("netstat","-a -b");
}


    // here we are going to create a function that will execute a properly formmated comand line prompt and
    // return anything that we would like to know.
    fn command(library:&str, command_string:&str)
    {
     let args_vec: Vec<&str> = command_string.split(' ').collect();
    let _child = Command::new(library)
        .args(args_vec)
        .status()
        .expect("failed to execute process");

   }

I couldn’t get this bit of code to run though:

match child {
   Ok(exit_code) => {
       // check return code, report command failure if non-zero...
        println!("netsh returned {}", exit_code.code());
    },
    Err(err) => {
        // handle the error.
        eprintln!("failed to execute: {}", err);
    },
}

exits with error:

error[E0308]: mismatched types
  --> src\main.rs:20:5
   |
20 |     Ok(exit_code) => {
   |     ^^^^^^^^^^^^^ expected struct `std::process::ExitStatus`, found enum `std::result::Result`
   |
   = note: expected type `std::process::ExitStatus`
              found type `std::result::Result<_, _>`

error[E0308]: mismatched types
  --> src\main.rs:24:5
   |
24 |     Err(err) => {
  |     ^^^^^^^^ expected struct `std::process::ExitStatus`, found enum `std::result::Result`
  |
  = note: expected type `std::process::ExitStatus`
          found type `std::result::Result<_, _>`

#5

Okay, let’s review some of the types here:

  • .status() returns Result<ExitStatus, io::Error>
  • .expect() on the Result returns ExitStatus, and panics on error.

@naim’s code is written under the assumption that child: Result<ExitStatus>; you can tell this from the Ok and Err in the match statement, and from calling code() on exit_code.

Basically: Their suggestion was for you to remove the call to expect().

You may wish to read about these error handling patterns in rust so you can gain a better grasp of what they were suggesting: See this chapter of TRPL.


let args_vec: Vec<&str> = command_string.split(' ').collect();

Seeing this gives me a very unclean feeling. I hope you understand the limitations of what you have written here, and the reason why arg and args normally take a separate string for each argument?


Overall I get the feeling that this is a poor candidate for a “first rust program.” Process management and interaction with the operating system is always some of the most frustrating stuff to write, because every API you deal with is going to be its own special little snowflake with unexpected and surprising behaviors. Trying to deal with all these little nuances at the same time that you are learning a new programming language seems… rough.


#6

Yes, it is. That’s why I’m trying to get feedback. I just started learning rust on friday of last week. Thank you for the pointers, I will continue going through documentation and asking questions. Unfortunately it’s a replacement for a program written a few years ago that for some reason or other won’t run on the computers anymore. So I’m having to rewrite it in another language. (I didn’t write the first one, but I do have the basic code)


#7

Okay, I’ve been able to get the os and the .net version so far. Working on other installed programs that need to be checked now.

fn dot_net_version() -> String
 {
   println!("Reading some system info...");
   let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
   let cur_ver = hklm.open_subkey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full\\1033").unwrap();
   let version: String = cur_ver.get_value("Version").unwrap();
   println!("{:?}",version );
  let return_str = version.to_string();
  return_str
}

and this checks for windows:

fn get_os() -> String
{
  let os = OS;
  let returnstr = os.to_string();
  returnstr
}

#8

Handling different OSs in Rust is easily done with conditional compilation:

#[cfg(windows)]
fn do_something() {
  // windows implementation
}

#[cfg(not(windows))]
fn do_something() {
  // alternative implementation
}

If you intend to actually use your program, you should pay more attention to error handling. Forget about unwrap() and expect() and properly handle the errors. Most (or even all) of your functions should return a Result of some sort. Some useful information about error handling can be found here.

One strange thing in your code above is that you use "-a -b" to create a string that contains all the arguments and later use split(' ') to break them to separate strings. It would be much more straightforward to not join them in a single thing in the first place. Something like this can work:

command("netstat", &["-a", "-b"]);

Programs don’t usually need to deal with string representation of commands. For most (or even all) OSs the command line arguments are really an array. This is also indicated by the fact that Rust’s std wants an array here. Only shells (e.g. bash or cmd.exe) have a concept of command as a single string, and they have to deal with all that splitting and escaping. You don’t need to do that.


#9

See, the problem is, whenever I try to send a command with a space and quotations… it crashes. For example this code runs fine:

fn make_firewall_tcp(name:&str, dir:&str, action:&str, protocol:&str, localport:&str){
let _child = Command::new("netsh")
.arg("advfirewall")
.arg("firewall")
.arg("add")
.arg("rule")
 .arg(r#"example_name"#)
 .arg(dir)
 .arg(action)
 .arg(protocol)
  .arg(localport)
   .spawn().expect("failed");
  thread::sleep(time::Duration::new(2, 0)); // Windows needs time!   
 println!("done");
 let _output = _child.stdout;
println!("{:?}", _output);

}

but this code breaks.

fn make_firewall_tcp(name:&str, dir:&str, action:&str, protocol:&str, localport:&str){
let _child = Command::new("netsh")
.arg("advfirewall")
.arg("firewall")
.arg("add")
.arg("rule")
 .arg(r#"example name"#)
 .arg(dir)
 .arg(action)
 .arg(protocol)
  .arg(localport)
   .spawn().expect("failed");
  thread::sleep(time::Duration::new(2, 0)); // Windows needs time!   
 println!("done");
 let _output = _child.stdout;
println!("{:?}", _output);
}

the only difference is the spaces. It is also why I have to add
.arg(“add”)
.arg(“rule”)

instead of what it should be (and what it is in native command prompt)
.arg(“add rule”)

Only other reference to this I could find was this overflow post which said that this was a known issue with rust. It has problems with escaping quotes in process command.

After fiddling around with it, it appears to me that if I add an “arg” for every space it seems to run fine. And if I take a valid command prompt: “advfirewall firewall add rule name=“my_name” dir=in action=allow protocol=TCP localport=19800-19820” and then split it with the spaces and turn it into an “args[array]” it again works fine.

I know this is a hack, and if anyone has a valid running example of a process command working with spaces and quotes in a more elegant way I’m open to suggestions.


#10

Hi @heplyler,
Rust can be cross compiled to multiple platforms (Operating Systems).
The code you are writing seems to be Windows specific.
GetVersion will allow you to check the current running Windows version. It can be called via the kernel32-sys crate.


#11

You may want to take this into consideration:
When a program is started, the command line is parsed. It splits the command line into an array of arguments.
Each argument is separated by a space or otherwise escaped in order to contain spaces or special characters.

In your code, you may want child to be mutable - and conditionally add args.

You may find this article useful: https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-commandlinetoargvw#remarks


#12

You don’t have to write quotes in your arguments. Instead of arg(r#"example name"#) you should have arg("example name"). This particular case should work. If the underlying API wants quotes there, Rust will add them for you. If you write quotes, Rust will try to escape them and you’ll get an incorrect result.

However, it’s true that there are some cases where it’s not possible to make Command escape your arguments correctly, as discussed in the link you found and in the linked issue. For example, arg(r#name="my name"#) may not work at all (although it’s possible that arg("name=my name") will work). In that case you’ll probably have to resort to raw calls to CreateProcess through the winapi crate or the space splitting hacks you mentioned.


#13

These are identical.

And this is invalid :stuck_out_tongue: Probably you meant

arg(r#"name="my name""#)

#14

@heplyler This works even with rules containing spaces, please try:

use std::io::Result;
use std::process::Command;

fn main() -> Result<()> {
    let name = "Remote Service Management (RPC)";
    let output = Command::new("netsh")
        .args(&["advfirewall", "firewall", "show", "rule"])
        .arg(format!("name={}", name))
        .output()?;

    if output.status.success() {
        println!("Command executed successfully");
        
        let stdout = String::from_utf8_lossy(&output.stdout);
        println!("--- stdout ---\n{}", stdout);
    } else {
        // this unwrap() should be fine on Windows
        println!("Process exited with status code {}", output.status.code().unwrap());
        
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        println!("--- stdout ---\n{}\n--- stderr ---\n{}", stdout, stderr);
    }

    Ok(())
}