Rubensei / windivert-rust

Rust bindings and wrapper around WinDivert user library
GNU Lesser General Public License v3.0
43 stars 9 forks source link

Statically linking to binary? #4

Closed ShayBox closed 1 year ago

ShayBox commented 1 year ago

Is there a way to statically link to my binary so I don't need the WinDivert.dll with it? I don't understand the build system and linking, it's possible to statically link WinDivert here and here

EDIT: The vendored feature should be enabled by default to make the library buildable and usable by default and reduce required intervention and setup by default, the alternative already requires many additional steps to download, extract, compile, and set an environment variable anyway, optionally disabling default features (vendored) is not a concern, you could also leave the logic the same which would allow providing WINDIVERT_PATH even while vendored is still enabled, not requiring the user to disable or enable features at all.

Copying the built WinDivert.dll to the binary directory relative to OUT_DIR (up a few parent directories) by default would make binaries usable by default (in addition to the above change) without any additional intervention (setting an arbitrary WINDIVERT_PATH environment variable).

EDIT2: Maybe a guard to uninstall the driver on drop/crash would be a good improvement.

Unrelated:

I created a build.rs for anyone that wants to use the officially built and signed binaries easier

use std::{fs::File, path::Path};

use anyhow::Result;
use zip::ZipArchive;

fn main() -> Result<()> {
    let out_dir = std::env::var("OUT_DIR")?;
    let out_dir_path = Path::new(&out_dir);

    let zip_name = "WinDivert-2.2.2-A.zip";
    let zip_path = out_dir_path.join(zip_name);

    if !zip_path.exists() {
        let base_url = "https://reqrypt.org/download";
        let file_url = format!("{base_url}/{zip_name}");

        let mut response = reqwest::blocking::get(file_url)?;
        let mut zip_file = File::create(&zip_path)?;
        std::io::copy(&mut response, &mut zip_file)?;
    }

    let zip_file = File::open(&zip_path)?;
    let mut zip_archive = ZipArchive::new(zip_file)?;
    zip_archive.extract(out_dir_path)?;

    let partial_file_names = [
        "x64/WinDivert.dll",
        "x64/WinDivert.lib",
        "x64/WinDivert32.sys",
        "x64/WinDivert64.sys",
        "x64/windivertctl.exe", // Good for debugging
    ];

    let full_file_names = zip_archive
        .file_names()
        .filter(|full_file_name| {
            partial_file_names
                .iter()
                .any(|partial_file_name| full_file_name.ends_with(partial_file_name))
        })
        .collect::<Vec<_>>();

    for fill_file_name in full_file_names {
        let full_file_path = out_dir_path.join(fill_file_name);
        let Some(partial_file_name) = fill_file_name.split('/').last() else {
            continue
        };

        let Ok(mut old_file) = File::open(full_file_path) else {
            continue
        };

        let Ok(mut new_file) = File::create(format!("{out_dir}/../../../{partial_file_name}")) else {
            continue
        };

        if std::io::copy(&mut old_file, &mut new_file).is_err() {
            println!("cargo:warning={old_file:?} -> {new_file:?} failed to copy")
        }
    }

    Ok(())
}
Rubensei commented 1 year ago

Currently it's not possible, a dll is the final object when compiling for dynamic linking. Static linking requires a different compilation process that produces a different object (a .lib different from the one currently being generated when vendoring, which is an import library file)

I'm currently working in a static feature that overrides the vendored one and builds the library for static linking. It's taking some time since I don't know too much about msvc and mingw toolchains and compiler/linker arguments, specially since statically compiling mingw it's not officially supported by WinDivert.

Related to the vendored feature not being enabled by default, you still need to provide the .sys file. It's not really possible to make it seamless to use in any case, and at that point I think it makes more sense to manually opt in instead of having it as the default behavior. I should add top level documentation about it in both crates though.

Build scripts should not make changes outside OUT_DIR. For development cargo run can properly run the binary using either WINDIVERT_PATH or the vendored compile output. For release it's possible to use WINDIVERT_DLL_OUTPUT to copy the files to a known path and automate the rest.

ShayBox commented 1 year ago

I realized after I tried it that the sys file was needed and that the driver has to be signed anyway, so vendored is really only useful to get the library to compile without manually setting up the required files, maybe vendored can additionally download the signed files and extract the sys, or use all the files from the archive instead of compiling which would make it completely autonomous.

I'm looking forward to the static feature, would that still require the sys file as-well? if so, maybe that previous suggestion would be a separate feature instead of being bundled into vendored so it works with static and vendored, if it gets added.

Library build scripts shouldn't but considering WinDivert has no standard install / install location at all and the files are required to be with the binary, the functionality makes sense to have for ease of use as an example file/script to include in your own project, but the above script needs refining.

Rubensei commented 1 year ago

Downloading in build scripts it's generally a bad practice that might break builds with no apparent reason (outdated/broken link, link updated to point to another incompatible version, isolated build environments with no internet connection or connection issues in general, etc)

Bundling the files with the library also has its cons, being the main one that it makes it harder for the user of the library to replace the used version with a compatible new one. It's something I might be open to add once I give it a good though and research a bit how it might impact the library usage and version management.

The driver sys file will be required even if statically linking, furthermore it will make it mandatory for the sys file to be in the same folder as the binary, since windivert dll library code only searches for the driver in the same folder the library itself is located.

ShayBox commented 1 year ago

For now I'm just gonna use my fork which replaces the WINDIVERT_DLL_OUTPUT env with a relative OUT_DIR path (just so I don't have to figure out a way to set an env or set it globally, not ideal) and switch to static if/when that comes out, and I'll move the downloading code from my build.rs to runtime and only have to download the sys file.

If there's a way to prevent the binary from crashing without the DLL file immediately then the downloading of the DLL file could also be handled in runtime (maybe with a download manager?) but for now, it crashes before any execution without the DLL, but not the sys file.

Rubensei commented 1 year ago

For now I'm just gonna use my fork...

I've just uploaded the WIP changes to feature/static-link, you can add the dependency via git instead of crates.io. It should work with msvc compiler, the gnu part needs more work.

... just so I don't have to figure out a way to set an env or set it globally ...

Most IDE allow for setting env vars for the current project quite easily. Special mention to vscode that allows to set it up for both the terminal and rust-analyzer

If there's a way to prevent the binary from crashing without the DLL file immediately then the downloading of the DLL file could also be handled in runtime

Although the sys file it's only required at runtime, the dll it's required for linking in the build process so it's neither possible nor something I'd consider adding, not even for the sys file.

ShayBox commented 1 year ago

Special mention to vscode that allows to set it up for both the terminal and rust-analyzer

I wasn't aware you could do that, only that you could set them via tasks, very useful!

The feature works for me, though without the sys file the error no longer says the SYS missing error, just Error: The system cannot find the path specified. (os error 3) which may not be as helpful.

EDIT: Nevermind, this was an unrelated issue, I needed to delete HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WinDivert

This is exactly what I needed 😄 Thank you

Rubensei commented 1 year ago

I'll keep this open until I finish the work on the static feature.

When there is a crash and the service does not properly stop, you can use sc stop WinDivert from an elevated command line to do it manually.

I still cannot decide whether or not I should add a Drop implementation that closes the handle since it is a fallible function and any error in drop has to be silently ignored.

ShayBox commented 1 year ago

I found that somewhere else but the windivert service doesn't exist for me

EDIT: Using uninstall can cause the driver to break and require a reboot to work again for some reason, I just let my program crash and it always works again the next time I start it without uninstalling as long as the sys file is in the same location

ShayBox commented 1 year ago

(Unrelated to static linking)
You wouldn't happen to have an example or library that could help parse HTTP packets, or a resource/video on how to do that, I'm having trouble using httparse/http_types to parse packets, I'm trying to sniff http/https json data requests/responses.

use smoltcp::wire::{IpProtocol, IpVersion, Ipv4Packet, TcpPacket};
use windivert::prelude::*;

fn main() {
    let filters = [
        "ip",
        "tcp",
        "tcp.PayloadLength > 1",
        "(tcp.DstPort == 80 || tcp.DstPort == 443)",
    ];

    let filter = filters.join(" && ");
    let priority = 0;
    let flags = WinDivertFlags::new().set_sniff();

    let windivert = match WinDivert::network(&filter, priority, flags) {
        Ok(windivert) => Ok(windivert),
        Err(error) => match &error {
            WinDivertError::Open(WinDivertOpenError::MissingSYS) => {
                download_and_extract_driver().expect("Failed to get driver");
                WinDivert::network(&filter, priority, flags)
            }
            _ => Err(error),
        },
    }
    .expect("Failed to initialize WinDivert<NetworkLayer>");

    loop {
        // ? I don't know what to set max as for buffer or packet count...
        const MAX: usize = u8::MAX as usize;

        let mut buffer = [0; MAX];
        while let Ok(packets) = windivert.recv_ex(Some(&mut buffer), MAX) {
            for packet in packets {
                // Theoretically these filters shouldn't be printing anything
                // Because WinDivert is pre-filtering the same things
                let payload = packet.data;
                let Ok(ip_version) = IpVersion::of_packet(&payload) else {
                    eprintln!("Failed to parse IpVersion");
                    continue;
                };
                if ip_version != IpVersion::Ipv4 {
                    eprintln!("Filtered out non-ipv4 version: {ip_version}");
                    continue;
                }

                let Ok(ipv4_packet) = Ipv4Packet::new_checked(&payload) else {
                    eprintln!("Failed to parse Ipv4Packet");
                    continue;
                };

                let protocol = ipv4_packet.next_header();
                if protocol != IpProtocol::Tcp {
                    eprintln!("Filtered out non-tcp protocol: {protocol}");
                    continue;
                }

                let payload = ipv4_packet.payload();
                let Ok(tcp_packet) = TcpPacket::new_checked(payload) else {
                    eprintln!("Failed to parse TcpPacket");
                    continue;
                };

                let payload = tcp_packet.payload();
                let payload_length = payload.len();
                if payload_length <= 1 {
                    eprintln!("Filtered out empty tcp payload: {payload_length}");
                    continue;
                }

                let destination_port = tcp_packet.dst_port();
                if ![80, 443].contains(&destination_port) {
                    eprintln!("Filtered out non-http destination port: {destination_port}");
                    continue;
                }

                println!("Payload: {payload:?}");
                // TODO: Parse HTTP?
            }
        }
    }
}
Rubensei commented 1 year ago

Released 0.10.0 of the sys crate and 0.6.0 of the wrapper crate with static feature