As I mentioned in my previous article Deconstructing SPF Records 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 entitled Rust Generic Types in Method Definitions. 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
#[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.
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
#[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.
#[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
#[derive(Debug, Clone)]
enum MechanismKind {
Include,
Redirect,
A,
MX,
IpV4,
IpV6,
All,
}
And I updated the SpfMechanism
to be
#[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.
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.
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()
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:...
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
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
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.