Unverified Commit 22eaead9 authored by Georg Koppen's avatar Georg Koppen
Browse files

Merge remote-tracking branch 'origin/merge-requests/25'

parents a6c16036 0449a489
Loading
Loading
Loading
Loading
+69 −9
Original line number Diff line number Diff line
@@ -16,8 +16,9 @@ Note: part of this section should probably be moved into code documentation.

    A `filter` can be:
    - `addr:<IP address>`
    - `ff:<fingerprints file>`
    - `fl:<flag>`
    - `fp:<fingprint`
    - `fp:<fingerprint>`
    - `p:<port>`
    - `v:<tor version>`
    A filter can be `exclude` (boolean), ie. not matching a filter, with the
@@ -28,6 +29,10 @@ Note: part of this section should probably be moved into code documentation.
    The output are the rules for `approved-routers.conf` in the form
    `!badexit <fp>`.

    If the filter is `ff`, it'll also include in `approved-routers.conf` the
    fingerprints that weren't found in the consensus. If a fingerprint could
    not be parsed, it'll be ignored but printed to the stdout.

    Examples:
    - `config badexit 25`, output:

@@ -152,26 +157,54 @@ Note: part of this section should probably be moved into code documentation.
  - `reject`: `Generate reject rule(s)` for the DirAuths. The parameters are
    a ticket number (in `bad-relay-reports` repo) and optionally some filters.

    It works as the previous command except that:
    - it also generates rules for `bad.conf` in the form `AuthDirReject <ip>`.
    - instead of generating rules for `approved-routers.conf` in the form
      `!badexit <fp>`, it generates rules like `!reject <fp>`.
    It works as the previous command but instead of generating rules for
    `approved-routers.conf` in the form `!badexit <fp>`, it generates rules
    like `!reject <fp>`.

    eg:
    - `config reject 25 p:8888`, output:

      ```bash
      [+] Rules for bad.conf:

      [+] Rules for approved-routers.d/approved-routers.conf:

      -----
      # Ticket: https://gitlab.torproject.org/tpo/network-health/bad-relay-reports/-/issues/25
      !reject 2BC31B73E0000B66981F7734D2B1F2C16C27D0BB
      !reject 673510F48FA7EBE1C21A9A32566AB9B7AA8EFC48
      !reject 94A8976E00C68ED23695D0668D87B3E7F126AF62
      -----

      [+] Found 3 relays: [FindFilter { exclude: false, filter: Port(8888) }]

      ```


  - `rejectbad <ticket_number> [filters]` : `Generate reject rule(s), writing to bad.conf too`.

    It works as the previous command but it also generates rules for
    `bad.conf` in the form `AuthDirReject <ip>` plus comments with the
    corresponding relays' fingerprints.

    eg:

    - `config rejectbad 25 p:8888`, output:
      ```bash
      [+] Rules for torrc.d/bad.conf:

      -----
      # Ticket: https://gitlab.torproject.org/tpo/network-health/bad-relay-reports/-/issues/25
      # Fingerprints:
      #               2BC31B73E0000B66981F7734D2B1F2C16C27D0BB
      #               5BE999DDB0916332AC21CE4AE9CED29FD7AAB284
      #               94A8976E00C68ED23695D0668D87B3E7F126AF62
      AuthDirReject 65.109.16.131
      AuthDirReject 104.200.30.152
      AuthDirReject 2600:3c03::f03c:93ff:fecc:2d20
      AuthDirReject 155.248.213.203
      -----

      [+] Rules for approved-routers.conf:
      [+] Rules for approved-routers.d/approved-routers.conf:

      -----
      # Ticket: https://gitlab.torproject.org/tpo/network-health/bad-relay-reports/-/issues/25
@@ -184,9 +217,36 @@ Note: part of this section should probably be moved into code documentation.

      ```

    - `config rejectbad 25 ff:testdata/fps.txt`, output:
      ```bash
      Errors parsing testdata/fps.txt: Wrong fingerprint length: 0123456789abcdef0123456789abcdef0123456
      Errors parsing testdata/fps.txt: Wrong fingerprint length: 0123456789abcdef0123456789abcdef0123456
      [+] Rules for torrc.d/bad.conf:

      -----

      # Ticket: https://gitlab.torproject.org/tpo/network-health/bad-relay-reports/-/issues/25
      # Fingerprints:
      #               0011BD2485AD45D984EC4159C88FC066E5E3300E
      AuthDirReject 162.247.74.201
      -----

      [+] Rules for approved-routers.d/approved-routers.conf:

      -----

      # Ticket: https://gitlab.torproject.org/tpo/network-health/bad-relay-reports/-/issues/25
      !reject 0011BD2485AD45D984EC4159C88FC066E5E3300E
      !reject 0123456789ABCDEF0123456789ABCDEF01234567
      -----

      [+] Found 1 relays: [FindFilter { exclude: false, filter: FpsFileFilter([Rsa("0011bd2485ad45d984ec4159c88fc066e5e3300e"), Rsa("0123456789abcdef0123456789abcdef01234567")]) }]

      ```

  - `middleonly`: `Generate middleonly rule(s)` for the DirAuths.
  - The parameters are a ticket number (in `bad-relay-reports` repo) and
  - optionally some filters.
    The parameters are a ticket number (in `bad-relay-reports` repo) and
    optionally some filters.

    It works as the previous commands except that:
    - it does not generate rules for `bad.conf`.
+87 −14
Original line number Diff line number Diff line
@@ -5,11 +5,12 @@ use std::fs;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::prelude::*;
use std::path::Path;
use std::path;
use structopt::StructOpt;

use crate::commands::err::Error;
use crate::commands::find;
use crate::commands::util;
use crate::commands::RunnableOffline;

static GITLAB_BUG_URL: &str =
@@ -55,8 +56,16 @@ pub struct ConfigCommand {
fn fmt_addr_rule(prefix: &str, relay: &tor_netdir::Relay<'_>) -> String {
    let rules: Vec<_> = relay
        .rs()
        // This returns all the ORPort IPs (v4 and v6)
        .orport_addrs()
        .map(|a| format!("{} {}", prefix, a.ip()))
        .map(|a| {
            if a.is_ipv4() {
                format!("{} {}", prefix, a.ip())
            } else {
                // Enclose v6 IPs in brackets
                format!("{} [{}]", prefix, a.ip())
            }
        })
        .collect();
    [rules.join("\n"), "\n".to_string()].concat()
}
@@ -104,7 +113,7 @@ impl BadCommand {
    }

    fn open_file(&self, fname: &str) -> Result<File, Error> {
        let path = Path::new(fname);
        let path = path::Path::new(fname);
        let parent = path
            .parent()
            .ok_or_else(|| Error::WrongParent(fname.to_string()))?;
@@ -133,6 +142,20 @@ impl BadCommand {
        Ok(())
    }

    fn print_missing_fps(
        &self,
        prefix: &str,
        file: &mut File,
        fps: Vec<String>,
    ) -> Result<(), anyhow::Error> {
        for fp in fps {
            let rule = format!("{} {}\n", prefix, fp);
            print!("{}", rule);
            file.write_all(rule.as_bytes())?;
        }
        Ok(())
    }

    fn generate(
        &self,
        netdir: &tor_netdir::NetDir,
@@ -141,8 +164,8 @@ impl BadCommand {
        let relays = find::FindCommand::new(&self.filters).filter(netdir);

        // Do not create bad.conf config when there is not token for it, as it
        // is the case for `middleonly` argument.
        if !tokens.0.is_empty() {
        // is the case for `middleonly` argument or when no relays were found.
        if !tokens.0.is_empty() && !relays.is_empty() {
            // When token.0 is present, write also into BAD_PATH
            let fname = BAD_PATH;
            let mut file = self.open_file(fname)?;
@@ -158,21 +181,52 @@ impl BadCommand {
            )?;
            // Print the addresses when there's the `AuthDirReject` token
            self.print_rules(tokens.0, &mut file, fmt_addr_rule, &relays)?;

            self.print_footer();
        }
        // Write into APPROVED_ROUTERS_PATH in any case
        // If the filter is `FpsFileFilter`, print all the parsed fingerprints
        // from the file even if the weren't found in the consensus.
        let found_fps = util::relays2fps(&relays);
        let missing_fps = self.missing_fps(found_fps);
        // Write into APPROVED_ROUTERS_PATH if there're found relays or
        // missing ones.
        if !relays.is_empty() || !missing_fps.is_empty() {
            let fname = APPROVED_ROUTERS_PATH;
            let mut file = self.open_file(fname)?;
            self.print_header(fname, &mut file)?;

            self.print_rules(tokens.1, &mut file, fmt_fp_rule, &relays)?;

            if !missing_fps.is_empty() {
                self.print_missing_fps(tokens.1, &mut file, missing_fps)?;
            }
            self.print_footer();

        }
        println!("[+] Found {} relays: {:?}", relays.len(), self.filters);
        Ok(())
    }

    /// Return the relays' fingerprints in the `FpsFileFiler` filter that were
    /// not found.
    /// Since `self` doesn't have a vector with the parsed fingeprints from
    /// the file, it is needed to parse the filters to obtain them.
    ///
    fn missing_fps(&self, found_fps: Vec<String>) -> Vec<String> {
        // NOTE: The following iterations and loops could be less verbose
        // Create a vector with the missing fps from the previous vector.
        let mut fps = Vec::new();
        for find_filter in &self.filters {
            if let find::Filter::FpsFileFilter(relay_fingerprints) =
                &find_filter.filter
            {
                for relay_fingerprint in relay_fingerprints {
                    if !found_fps.contains(&relay_fingerprint.to_string()) {
                        fps.push(relay_fingerprint.to_string())
                    }
                }
            }
        }
        fps
    }
}

#[async_trait]
@@ -216,8 +270,8 @@ mod tests {
        let file = bad_command.open_file(fname);
        assert!(file.is_err());
    }
    #[test]

    #[test]
    fn print_header_ok() {
        let binding = temp_dir().join("approved-routers.conf");
        let fname = binding.to_str().unwrap();
@@ -233,4 +287,23 @@ mod tests {
        let result = bad_command.print_header(fname, &mut file);
        assert!(result.is_ok());
    }

    #[test]
    fn missing_relays_ok() {
        let path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("testdata/fps.txt");
        let bad_command = BadCommand {
            ticket: 1,
            filters: Vec::from([find::FindFilter::new(
                false,
                find::Filter::FpsFileFilter(util::fpfile2fps(&path).unwrap()),
            )]),
        };
        let found_fps =
            vec!["0011BD2485AD45D984EC4159C88FC066E5E3300E".to_string()];
        let missing_fps = bad_command.missing_fps(found_fps);
        let expected_missing_fps =
            vec!["0123456789ABCDEF0123456789ABCDEF01234567".to_string()];
        assert!(expected_missing_fps == missing_fps);
    }
}
+10 −1
Original line number Diff line number Diff line
@@ -29,12 +29,13 @@ pub enum Filter {
    Version(String),
    /// Port policy
    PortPolicyFilter(PortPolicy),
    FpsFileFilter(Vec<util::RelayFingerprint>),
}

#[derive(Debug, Clone)]
pub struct FindFilter {
    exclude: bool,
    filter: Filter,
    pub filter: Filter,
}

impl FindFilter {
@@ -66,6 +67,11 @@ impl FindFilter {
            Filter::PortPolicyFilter(pp) => &**relay.md().ipv4_policy() == pp,
            // ^ this is `&Arc<PortPolicy>`, 1st dereference `Arc`,
            // then `&`, then add `&` to match `&PortPolicy`
            // The following will try to find the relay in the list of parsed
            // fingerprints from a file.
            Filter::FpsFileFilter(ff) => {
                ff.iter().any(|fp| fp.match_relay(relay))
            }
        };
        ret ^= self.exclude;
        ret
@@ -143,6 +149,9 @@ impl FromStr for FindFilter {
                "pf" | "portpolicyfile" => Filter::PortPolicyFilter(
                    util::portpolicyfile2portpolicy(Path::new(kv.1))?,
                ),
                "ff" | "fingerprintfile" => {
                    Filter::FpsFileFilter(util::fpfile2fps(Path::new(kv.1))?)
                }
                _ => return Err(Error::UnrecognizedFilter(kv.0.to_string())),
            };
            return Ok(FindFilter::new(exclude, filter));
+41 −2
Original line number Diff line number Diff line
@@ -42,8 +42,10 @@ impl FromStr for RelayFingerprint {
impl fmt::Display for RelayFingerprint {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            RelayFingerprint::Rsa(rsa) => rsa,
            RelayFingerprint::Ed(ed) => ed,
            // Convert to uppercase so that when they are printend or converted
            // `to_string`, they're always uppercase
            RelayFingerprint::Rsa(rsa) => rsa.to_uppercase(),
            RelayFingerprint::Ed(ed) => ed.to_uppercase(),
        };
        write!(f, "{}", s)
    }
@@ -87,6 +89,43 @@ pub fn portpolicyfile2portpolicy(path: &Path) -> Result<PortPolicy, Error> {
    Ok(portpolicy)
}

/// Convert fingerprints from a file into a Vector of RelayFingerprints
///
pub fn fpfile2fps(path: &Path) -> Result<Vec<RelayFingerprint>, Error> {
    let pathbuf = PathBuf::from(path);
    let content = read_to_string(pathbuf)?;
    let parts: Vec<_> = content.split_whitespace().collect();
    let (fingerprints, errors): (Vec<_>, Vec<_>) = parts
        .iter()
        .map(|part| part.parse::<RelayFingerprint>())
        .partition(Result::is_ok);
    if !errors.is_empty() {
        println!(
            "Errors parsing {}: {}",
            path.display(),
            errors
                .iter()
                .map(|e| e.as_ref().unwrap_err().to_string())
                .collect::<Vec<_>>()
                .join("\n")
        );
    }
    Ok(fingerprints.into_iter().map(Result::unwrap).collect())
}

/// Convert a Relay Vector into an String Vectors, with the relays' Rsa
/// fingerprints.
///
pub fn relays2fps(relays: &[tor_netdir::Relay]) -> Vec<String> {
    let found_fps: Vec<_> = relays
        .iter()
        .map(|relay| {
            relay.rsa_id().to_string().replace('$', "").to_uppercase()
        })
        .collect();
    found_fps
}

fn get_version(r: &tor_netdir::Relay) -> String {
    // Can't `unwrap_or` cause can't create `Version` data type
    r.rs()

testdata/fps.txt

0 → 100644
+3 −0
Original line number Diff line number Diff line
0011bd2485ad45d984ec4159c88fc066e5e3300e
0123456789abcdef0123456789abcdef0123456
0123456789abcdef0123456789abcdef01234567