Skip to content

Commit

Permalink
Configuration Wizard
Browse files Browse the repository at this point in the history
Implementation of a simple configuration wizard. This wizard asks the user for
the general parameters in the configuration file; after the user completes the
wizard, a configuration file is written and the application proceeds normally.

Also adding a README file and cross-compile tips.
  • Loading branch information
alessandroasm committed Jun 26, 2020
1 parent e8cb24f commit 96c08d6
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 20 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Rust AWS DDNS

This project implements a simple alternative to DDNS services using AWS Route53
as the DNS provider. It achieves that by discovering the machine public internet
ip addresses (both IPv4 and IPv6) and then updates the RecordSets on the
configured Route53 account.

The tool can run on small devices, like raspberry pis, and automatically with
a simple cron entry.
66 changes: 66 additions & 0 deletions cross-compile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## Cross compilation

This project can be cross compiled to another targets like raspberry pis
(armv7). In order to do so, follow the steps on this document.

**This document is incomplete. The compiled application fails because it is
dynamically linked to GLIBC. We must use Musl somehow in order to generate
a statically linked application.**

### Installing the toolchain

These steps were copied from [japaric's rust-cross](https://github.com/japaric/rust-cross)
and assumes Ubuntu 20.04.

```
# Step 0: Our target is an ARMv7 device, the triple for this target is `armv7-unknown-linux-gnueabihf`
# Step 1: Install the C cross toolchain
$ sudo apt-get install -qq gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
# Step 2: Install the cross compiled standard crates
$ rustup target add armv7-unknown-linux-gnueabihf
$ rustup target add armv7-unknown-linux-musleabihf
# Step 3: Configure cargo for cross compilation
$ mkdir -p ~/.cargo
$ cat >>~/.cargo/config <<EOF
> [target.armv7-unknown-linux-gnueabihf]
> linker = "arm-linux-gnueabihf-gcc"
>
> [target.armv7-unknown-linux-musleabihf]
> linker = "arm-linux-musleabihf-gcc"
> EOF
# Test cross compiling a Cargo project
$ cargo new --bin hello
$ cd hello
$ cargo build --target=armv7-unknown-linux-gnueabihf
Compiling hello v0.1.0 (file:///home/ubuntu/hello)
```

### OpenSSL

It is necessary to have a OpenSSL compilation for the specified target. The
following steps assume that openssl will be cloned to ~/src

```
# Step 0: clone OpenSSL (v1.1.1-stable)
$ git clone -b OpenSSL_1_1_1-stable https://github.com/openssl/openssl.git
# Step 1: configure for building
$ ./Configure linux-armv4 --prefix=/usr/local/openssl/ --openssldir=/usr/local/openssl shared
# Step 2: build using the gcc cross compiler
$ make CC=arm-linux-gnueabihf-gcc-9
```

### Building the application

```
PKG_CONFIG_ALLOW_CROSS=1 \
OPENSSL_LIB_DIR=~/src/openssl \
OPENSSL_INCLUDE_DIR=~/src/openssl/include \
OPENSSL_STATIC=1 \
cargo build --release --target=armv7-unknown-linux-gnueabihf
```
7 changes: 5 additions & 2 deletions src/aws_credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ struct AwsCsvEntry {
}

pub fn from_csv(csv_file: &str) -> Option<AppAwsCredentials> {
let csv_file = std::fs::File::open(csv_file).unwrap();
let csv_file = std::fs::File::open(csv_file);
if csv_file.is_err() {
return None;
}

let mut csv_rdr = csv::Reader::from_reader(csv_file);
let mut csv_rdr = csv::Reader::from_reader(csv_file.unwrap());
for row in csv_rdr.deserialize::<AwsCsvEntry>() {
if let Ok(record) = row {
println!("Record: {:?}", record);
Expand Down
174 changes: 171 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,186 @@ pub struct AppConfig {
pub update_ipv6: bool,

pub provider_v4: Option<String>,

pub aws_access_key: Option<String>,
pub aws_secret_access_key: Option<String>,
}

impl AppConfig {
pub fn parse(config_file: &str) -> Option<Self> {
pub async fn parse(config_file: &str) -> Option<Self> {
// Opening and parsing the configuration file
let f = std::fs::File::open(config_file);
if f.is_err() {
return None;
if let Err(io_err) = f {
use std::io::ErrorKind;

return match io_err.kind() {
ErrorKind::NotFound => {
println!(
"Config file not found! Starting configuration wizard\n"
);
AppConfig::run_config_wizard(config_file).await
}
_ => None,
};
}

let f = f.unwrap();
let config: AppConfig = serde_yaml::from_reader(f).unwrap();
Some(config)
}

/// Starts a wizard to generate a valid configuration file
async fn run_config_wizard(config_file: &str) -> Option<Self> {
loop {
// AWS Access Key
print!("AWS Access Key [Blank for env / system credentials]: ");
let aws_access_key = read_line();
let aws_access_key = String::from(aws_access_key.trim());

let (aws_access_key, aws_secret_key) = if aws_access_key.len() > 0 {
print!("AWS Secret Key: ");
let secret_key = read_line();
let secret_key = String::from(secret_key.trim());

(Some(aws_access_key), Some(secret_key))
} else {
(None, None)
};

// Fetch hosted zones
let hosted_zone_id: &str;
let hosted_zone_name: &str;
let credentials = if aws_access_key.is_none() {
None
} else {
let access_key = String::from(aws_access_key.as_ref().unwrap());
let secret_key = String::from(aws_secret_key.as_ref().unwrap());

Some(crate::aws_credentials::AppAwsCredentials {
access_key: access_key,
secret_access_key: secret_key,
})
};

let client = crate::route53_client::Route53Client::new(credentials);
let hosted_zones = client.list_hosted_zones().await;

if hosted_zones.is_none() {
// It wasn't possible to fetch hosted zones. Restart the wizard
println!("\nThere was an error fetching Route53 hosted zones. Check your credentials\n");
continue;
}

let hosted_zones = hosted_zones.unwrap();
if hosted_zones.len() == 0 {
panic!("There are no configured Hosted Zones on this Route 53 account. Please create a hosted zone and run this application again.");
}

// Ask for zone_id
loop {
println!("\nSelect the desired Hosted Zone:");
hosted_zones.iter().enumerate().for_each(|entry| {
println!(
"{}. {} ({})",
entry.0 + 1,
(entry.1).1,
(entry.1).0
)
});

let hosted_zone_idx = read_int("Hosted Zone: ");
if hosted_zone_idx > 0 {
let hosted_zone_idx = hosted_zone_idx as usize;
if hosted_zone_idx <= hosted_zones.len() {
let hosted_zone = &hosted_zones[hosted_zone_idx - 1];
hosted_zone_id = &hosted_zone.0;
hosted_zone_name = &hosted_zone.1;

break;
}
}
}

// Ask for record_sets
let update_ipv4 = read_int("Update IPv4 (0 - No): ") != 0;
let update_ipv6 = read_int("Update IPv6 (0 - No): ") != 0;

let prompt =
format!("IPv4 record set prefix (xxx.{}): ", hosted_zone_name);
let record_set_v4 = read_non_blank_line(&prompt).to_lowercase();
let record_set_v4 =
format!("{}.{}", record_set_v4, hosted_zone_name);

let mut record_set_v6: Option<String> = None;
if update_ipv6 {
print!("IPv6 record set prefix (xxx.{}): ", hosted_zone_name);
let record_set_v6_str = read_line().trim().to_lowercase();

record_set_v6 = if record_set_v6_str.len() == 0 {
None
} else {
Some(format!("{}.{}", record_set_v6_str, hosted_zone_name))
};
}

// Write configuration out
let config = AppConfig {
zone_id: String::from(hosted_zone_id),
record_set: record_set_v4,
record_set_v6: record_set_v6,
update_ipv4: update_ipv4,
update_ipv6: update_ipv6,

provider_v4: None,

aws_access_key: aws_access_key,
aws_secret_access_key: aws_secret_key,
};

let file = std::fs::File::create(config_file).unwrap();
serde_yaml::to_writer(file, &config).unwrap();

return Some(config);
}
}
}

fn read_line() -> String {
use std::io::{self, BufRead, Write};

let mut stdout = io::stdout();
stdout.flush().unwrap();

let mut line = String::new();
let stdin = io::stdin();

stdin.lock().read_line(&mut line).unwrap();
line
}

fn read_int(prompt: &str) -> i32 {
loop {
print!("{}", prompt);

let line = read_line();
let line = line.trim();

let n = line.parse::<i32>();
if let Ok(n) = n {
return n;
}
}
}

fn read_non_blank_line(prompt: &str) -> String {
loop {
print!("{}", prompt);

let line = read_line();
let line = line.trim();

if line.len() > 0 {
return String::from(line);
}
}
}
29 changes: 19 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use clap::App;

mod aws_credentials;
use aws_credentials::AppAwsCredentials;

mod ip_address;
use ip_address::MyIpProvider;
Expand All @@ -27,19 +26,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.value_of("config")
.unwrap_or("rust-aws-ddns.yml");
let app_config = config::AppConfig::parse(config_file);
println!("Config: {:?}", &app_config);
let app_config = app_config.await.unwrap();

// Get API credentials
let mut credentials: Option<AppAwsCredentials> = None;
credentials = aws_credentials::from_csv("aws_user_credentials.csv");

// if credentials.is_none() {
// panic!("No AWS credentials found");
// }
let credentials_file = clap_matches
.value_of("csv")
.unwrap_or("aws_user_credentials.csv");
let mut credentials = aws_credentials::from_csv(credentials_file);

if credentials.is_none()
&& app_config.aws_access_key.is_some()
&& app_config.aws_secret_access_key.is_some()
{
let access_key = app_config.aws_access_key.as_ref().unwrap();
let secret_access_key =
app_config.aws_secret_access_key.as_ref().unwrap();

credentials = Some(aws_credentials::AppAwsCredentials {
access_key: String::from(access_key),
secret_access_key: String::from(secret_access_key),
});
}

// Checking and updating IPs
let route53_client = route53_client::Route53Client::new(credentials);
let app_config = app_config.unwrap();

// IPv4 First
if app_config.update_ipv4 {
Expand Down Expand Up @@ -81,7 +91,6 @@ async fn update_record_set(
) -> Result<(), Box<dyn std::error::Error>> {
// Get current IP Address
let my_ipaddr = ip_address::current(ip_provider).await?;
//client.list_hosted_zones().await;

client
.set_ip_address(&config.zone_id, record_set, &my_ipaddr)
Expand Down
36 changes: 31 additions & 5 deletions src/route53_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,46 @@ impl Route53Client {
}

fn new_client(&self) -> AwsRoute53Client {
let client = AwsRoute53Client::new(Region::UsEast1);
client
let region = Region::UsEast1;
match &self.credentials {
Some(cred) => {
let dispatcher = rusoto_core::HttpClient::new()
.expect("Failed to create Rusoto HTTP Client");
let provider = rusoto_core::credential::StaticProvider::new(
String::from(&cred.access_key),
String::from(&cred.secret_access_key),
None,
None,
);
AwsRoute53Client::new_with(dispatcher, provider, region)
}
_ => AwsRoute53Client::new(region),
}
}

pub async fn list_hosted_zones(&self) {
pub async fn list_hosted_zones(&self) -> Option<Vec<(String, String)>> {
let client = self.new_client();

let request = rusoto_route53::ListHostedZonesRequest {
delegation_set_id: None,
marker: None,
max_items: None,
};
let result = client.list_hosted_zones(request).await.unwrap();
println!("res: {:?}", &result);

let result = client.list_hosted_zones(request).await;
match result {
Err(_) => None,
Ok(res) => {
let v = res
.hosted_zones
.iter()
.map(|zone| {
(String::from(&zone.id), String::from(&zone.name))
})
.collect();
Some(v)
}
}
}

pub async fn set_ip_address(
Expand Down

0 comments on commit 96c08d6

Please sign in to comment.