Deconstructing SPF with Rust using Generics
Tags: rust, dns, spf, generic
Published: 2021-04-10 14:57:39


As I mentioned in my previous article [Deconstructing SPF Records with Rust](https://bas-man.github.io/post/deconstructing-spf-with-rust/). There is a case for using `generics` given the amount of overlap between the different mechanisms.

 So in this article I will document how I transitioned from using unique structs for each `mechanism`, leading to less overall code and other benefits.
 
 While I was considering how best to go about doing generics, I came across this very nice write up on [dev.to](https://dev.to) entitled [Rust Generic Types in Method Definitions](https://dev.to/talzvon/rust-generic-types-in-method-definitions-4iah). It's certainly worth a read. 

## A Refresher on the issue
Let's revisit some of the code from the previous article. Specifically we will just look at the `struct` for `Include` and `Ip4`: 

### Include
```rust
#[derive(Default, Debug, Clone)]
struct Include {
    qualifier: char,
    txt: String,
}
```

This is pretty straight forward. It holds a `qualifier` of `char`, and a `txt` of `String`. Other `mechanism`s in SPF use the exact same basic components. Specifically, `A` and `MX`. `Redirect` also uses the same components, with the `qualifier` always being `+` or simply *blank*. But I need to duplicate this `struct` definition for each `mechanim`.

Not only do I need to create the same structs, I also need to then `impl` the same functions on each struct.

```rust
impl Include {
    fn new(qualifier: char, txt: String) -> Self {
        Self { qualifier, txt }
    }
    fn as_mechanism(&self) -> String {
        // rebuild and return the string represensation of a include mechanism
        let mut txt = String::new();
        if self.qualifier != '+' {
            txt.push(self.qualifier);
        }
        txt.push_str("include:");
        txt.push_str(self.txt.as_str());
        txt
    }
    fn is_pass(&self) -> bool {
        if self.qualifier == '+' {
            true
        } else {
            false
        }
    }
    fn is_fail(&self) -> bool {
        if self.qualifier == '-' {
            true
        } else {
            false
        }
    }
    fn is_softfail(&self) -> bool {
        if self.qualifier == '~' {
            true
        } else {
            false
        }
    }
    fn is_neutral(&self) -> bool {
        if self.qualifier == '?' {
            true
        } else {
            false
        }
    }
}
```

This also applies to `ip4` and `ip6`
### Ip4
```rust
#[derive(Debug, Clone)]
struct Ip4 {
    qualifier: char,
    ip: IpNetwork,
}

impl Ip4 {
    fn new(qualifier: char, ip: IpNetwork) -> Self {
        Self { qualifier, ip }
    }
    fn as_string(&self) -> String {
        self.ip.to_string()
    }
    fn as_mechanism(&self) -> String {
        let mut ip4_string = String::new();
        if self.qualifier != '+' {
            ip4_string.push(self.qualifier);
        }
        ip4_string.push_str("ip4:");
        ip4_string.push_str(self.ip.to_string().as_str());
        ip4_string
    }
    fn as_ip(&self) -> IpNetwork {
        self.ip
    }
    fn is_pass(&self) -> bool {
        if self.qualifier == '+' {
            true
        } else {
            false
        }
    }
    fn is_fail(&self) -> bool {
        if self.qualifier == '-' {
            true
        } else {
            false
        }
    }
    fn is_softfail(&self) -> bool {
        if self.qualifier == '~' {
            true
        } else {
            false
        }
    }
    fn is_neutral(&self) -> bool {
        if self.qualifier == '?' {
            true
        } else {
            false
        }
    }
}
```

## Doing things with Generics

My initial idea was to simply make a generic struct as follows.

```rust
#[derive(Debug, Clone)]
struct SpfMechanism<T> {
    qualifier: char,
    mechanism: T,
}
```
And then impliment the functions that would apply to all instance types of `SpfMechanism`. But as I sat thinking about it. I realised it wouldn't work. Because the `as_mechanism()` function would need to be able to differentiate between a redirect, include, and so on. I needed to be able to use `as_mechanism` and create records similar to `include:_spf.example.com`, `ip4:X.X.X.X`, `a`, `mx:mx-host.example.com` and so on.  

I realised I would need to actually have a unique type/kind for each `mechanism`, and so I added an `enum`; listing the kinds of `mechanisms` 

```rust
#[derive(Debug, Clone)]
enum MechanismKind {
    Include,
    Redirect,
    A,
    MX,
    IpV4,
    IpV6,
    All,
}
```
And I updated the `SpfMechanism` to be
```rust
#[derive(Debug, Clone)]
struct SpfMechanism<T> {
    kind: MechanismKind,
    qualifier: char,
    mechanism: T,
}

```

Throw in a little magic so I can test the what `Kind` the `SpfMechanism` is:  
Note: This was generated by my editor with a click of a button.
```rust
impl MechanismKind {
    /// Returns `true` if the mechanism_kind is [`Include`].
    fn is_include(&self) -> bool {
        matches!(self, Self::Include)
    }
    /// Returns `true` if the mechanism_kind is [`A`].
    fn is_a(&self) -> bool {
        matches!(self, Self::A)
    }

    /// Returns `true` if the mechanism_kind is [`MX`].
    fn is_mx(&self) -> bool {
        matches!(self, Self::MX)
    }

    /// Returns `true` if the mechanism_kind is [`IpV4`].
    fn is_ip_v4(&self) -> bool {
        matches!(self, Self::IpV4)
    }

    /// Returns `true` if the mechanism_kind is [`IpV6`].
    fn is_ip_v6(&self) -> bool {
        matches!(self, Self::IpV6)
    }

    /// Returns `true` if the mechanism_kind is [`All`].
    fn is_all(&self) -> bool {
        matches!(self, Self::All)
    }

    /// Returns `true` if the mechanism_kind is [`Redirect`].
    fn is_redirect(&self) -> bool {
        matches!(self, Self::Redirect)
    }
}
```

Next I wanted to implement the functions that would be applied to all `SpfMechanism` irrespective of the `mechanism` type, be it `String` or `IpNetwork`

All `mechanism` types support these functions.
```rust
impl<T> SpfMechanism<T> {
    fn new(kind: MechanismKind, qualifier: char, mechanism: T) -> Self {
        Self {
            kind,
            qualifier,
            mechanism,
        }
    }
    fn is_pass(&self) -> bool {
        self.qualifier == '+'
    }
    fn is_fail(&self) -> bool {
        self.qualifier == '-'
    }
    fn is_softfail(&self) -> bool {
        self.qualifier == '~'
    }
    fn is_neutral(&self) -> bool {
        self.qualifier == '?'
    }
    fn mechanism_prefix_from_kind(&self) -> String {
        let push_str = match self.kind {
            MechanismKind::Redirect => "redirect=",
            MechanismKind::Include => "include:",
            MechanismKind::A => "a:",   // requires modification
            MechanismKind::MX => "mx:", // requires modication
            MechanismKind::IpV4 => "ip4:",
            MechanismKind::IpV6 => "ip6:",
            MechanismKind::All => "",
        };
        push_str.to_string()
    }
}
```
The important thing to note here is that `impl<T>` is what makes these functions able to work no matter what `<T>` ultimately turns out to be.

### Implementing SpfMechanism for redirect, include, a, and mx
These `mechanism` all share a common type of `String` for `T` but we do not need to recode the `is_*` functions. So we need to implement `as_string()` and `as_mechanism()` only at this time. You might noticed as you read through that `as_mechanism()` makes use of the `mechanism_prefix_from_kind()` function which is defined for the generic `SpfMechanism`

I also decided to implement `new_*` for each `mechanism` type. These are essentially aliases to `SpfMechanism::new()`

```rust 
impl SpfMechanism<String> {
    fn new_include(qualifier: char, mechanism: String) -> Self {
        SpfMechanism::new(MechanismKind::Include, qualifier, mechanism)
    }
    fn new_redirect(qualifier: char, mechanism: String) -> Self {
        SpfMechanism::new(MechanismKind::Redirect, qualifier, mechanism)
    }
    fn new_all(qualifier: char, mechanism: String) -> Self {
        SpfMechanism::new(MechanismKind::All, qualifier, mechanism)
    }
    fn as_mechanism(&self) -> String {
        // rebuild and return the string representation of a include, redirect, a or mx mechanism
        let mut txt = String::new();
        if self.qualifier != '+' {
            txt.push(self.qualifier);
        } else {
            // Do nothing omitting '+'
        }
        if self.kind.is_all() {
            txt.push_str("all")
        } else {
            txt.push_str(self.mechanism_prefix_from_kind().as_str());
            txt.push_str(self.mechanism.as_str());
        }
        txt
    }
    fn as_string(&self) -> &String {
        &self.mechanism
    }
}
```

The real payoff here is that we have a single `as_mechanism()` and `as_string()` which works for all `SpfMechanism` where `<T>` is of type `String`. Those being `redirect`, `include` `a`, `mx` and `all`. Within `as_mechanism()`, I call `mechanism_prefix_from_kind()` to get the correct prefix string, be it `include:`, `redirect=` and so on.

### Implementing SpfMechanism for Ip4 and Ip6

Ip4 and Ip6 are both of type `IpNetwork` the only difference being they are written as `ip4:X.X.X.X` and `ip6:xxxx:xxxx:...`

```rust
impl SpfMechanism<IpNetwork> {
    fn new_ip4(qualifier: char, mechanism: IpNetwork) -> Self {
        SpfMechanism::new(MechanismKind::IpV4, qualifier, mechanism)
    }
    fn new_ip6(qualifier: char, mechanism: IpNetwork) -> Self {
        SpfMechanism::new(MechanismKind::IpV6, qualifier, mechanism)
    }
    fn as_mechanism(&self) -> String {
        // rebuild and return the string represensation of a include, redirect mechanism
        let mut txt = String::new();
        if self.qualifier != '+' {
            txt.push(self.qualifier);
        } else {
            // Do nothing omitting '+'
        }
        txt.push_str(self.mechanism_prefix_from_kind().as_str());
        txt.push_str(self.mechanism.to_string().as_str());
        txt
    }
    fn as_string(&self) -> String {
        self.mechanism.to_string()
    }
}
```

In my previous article I didn't actually implement the `ip6:` records.
Now, using this more generic approach, by adding merely a few lines of code. I have been able to implement `ip6`

### Test Output for _netblocks2.google.com

```zsh
List of TXT records found for _netblocks2.google.com.
TXT Record 1:
v=spf1 ip6:2001:4860:4000::/36 ip6:2404:6800:4000::/36 ip6:2607:f8b0:4000::/36 ip6:2800:3f0:4000::/36 ip6:2a00:1450:4000::/36 ip6:2c0f:fb50:4000::/36 ~all

Decontructing SPF Record
Spf1 { source: "v=spf1 ip6:2001:4860:4000::/36 ip6:2404:6800:4000::/36 ip6:2607:f8b0:4000::/36 ip6:2800:3f0:4000::/36 ip6:2a00:1450:4000::/36 ip6:2c0f:fb50:4000::/36 ~all", include: None, redirect: None, is_redirected: false, a: None, mx: None, ip4: None, ip6: Some([SpfMechanism { kind: IpV6, qualifier: '+', mechanism: V6(Ipv6Network { addr: 2001:4860:4000::, prefix: 36 }) }, SpfMechanism { kind: IpV6, qualifier: '+', mechanism: V6(Ipv6Network { addr: 2404:6800:4000::, prefix: 36 }) }, SpfMechanism { kind: IpV6, qualifier: '+', mechanism: V6(Ipv6Network { addr: 2607:f8b0:4000::, prefix: 36 }) }, SpfMechanism { kind: IpV6, qualifier: '+', mechanism: V6(Ipv6Network { addr: 2800:3f0:4000::, prefix: 36 }) }, SpfMechanism { kind: IpV6, qualifier: '+', mechanism: V6(Ipv6Network { addr: 2a00:1450:4000::, prefix: 36 }) }, SpfMechanism { kind: IpV6, qualifier: '+', mechanism: V6(Ipv6Network { addr: 2c0f:fb50:4000::, prefix: 36 }) }]), all_qualifier: '~' }
SPF1: v=spf1 ip6:2001:4860:4000::/36 ip6:2404:6800:4000::/36 ip6:2607:f8b0:4000::/36 ip6:2800:3f0:4000::/36 ip6:2a00:1450:4000::/36 ip6:2c0f:fb50:4000::/36 ~all

There are no include elements
There are no ip4 networks
There are no ip4 spf records.
List of ip6 networks/hosts:
2001:4860:4000::/36
2404:6800:4000::/36
2607:f8b0:4000::/36
2800:3f0:4000::/36
2a00:1450:4000::/36
2c0f:fb50:4000::/36

List of ip6 mechanisms:
ip6:2001:4860:4000::/36
ip6:2404:6800:4000::/36
ip6:2607:f8b0:4000::/36
ip6:2800:3f0:4000::/36
ip6:2a00:1450:4000::/36
ip6:2c0f:fb50:4000::/36

Is a redirect: false
```

## Code
The code for this aritcle can be found [here](https://github.com/Bas-Man/learning-rust-trust-dns-resolver/tree/SPF-GENERIC)
## Final thoughts and thanks.

First, I would like to thank the people in the rust discord for answering my questions and making suggestions.

Also, this might not be the best solution. Exploring `traits` might be another way to go about getting things done.

Next steps?  
I will consider breaking this single file up into seperate files to make the management easier. And learning to write actual tests. Always better to catch issues early.