Skip to content

Commit

Permalink
Merge pull request #52 from junkurihara/feat/tcp-bootstrap
Browse files Browse the repository at this point in the history
Feat: implement tcp/udp bootstrap resolver with arbitrary port other than UDP over 53
  • Loading branch information
junkurihara authored Jan 23, 2024
2 parents 1780303 + 5a89d13 commit 5f96a96
Show file tree
Hide file tree
Showing 15 changed files with 421 additions and 86 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@
You should also include the user name that made the change.
-->

## 0.3.2

### Improvements

- Feat: Support TCP and UDP bootstrap DNS protocols. In addition to the existing format like "1.1.1.1" that means "udp://1.1.1.1:53", formats like "tcp://1.1.1.1:53" (TCP support) "udp://1.2.3.4:50053" (Non standard port) works. See "README.md" and "doh-auth-proxy.toml" for configuration for the detail.
- Refactor: Lots of minor improvements

## 0.3.1

### Bugfix

- Fix several error handlers

## 0.3.0

### Improvements
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Jun Kurihara
Copyright (c) 2024 Jun Kurihara

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ github.com. 60 IN A 52.69.186.44
~~~~~~~
```

The parameter `bootstrap-dns` is used to resolve the IP address of the host of `target-url` (i.e., target DoH server).
The parameter `bootstrap_dns` is used to resolve the IP address of the host of `target_urls` (i.e., target DoH server). The `bootstrap_dns` allows non-standard DNS ports other than `53` and TCP queries, which can be specified as an url-like format, e.g., `tcp://1.1.1.1`, `tcp://127.0.0.1:12345`, `127.0.0.1:50053`, where UDP and port `53` are used if omitted.

If you run without `--config` option, i.e., simply hit `$ ./doh-auth-proxy`, the followings are applied as default parameters:

Expand Down
6 changes: 4 additions & 2 deletions doh-auth-proxy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
## Address to listen to.
listen_addresses = ['127.0.0.1:50053', '[::1]:50053']

## DNS (Do53) resolver addresses for bootstrap
bootstrap_dns = ["8.8.8.8", "1.1.1.1"]
## DNS (Do53) resolver addresses for bootstrap.
## You can omit protocol name and port number, default is udp over port 53.
## The first one is used for bootstrap, and the rest are used for fallback as ordered.
bootstrap_dns = ["udp://8.8.8.8:53", "1.1.1.1:53", "8.8.4.4", "tcp://1.0.0.1"]

## Minutes to re-resolve the IP addr of the nexthop and authentication endpoint url
## Ip addresses are first resolved by bootstrap DNS, after that, they will be resolved by (MO)DoH resolver itself.
Expand Down
6 changes: 3 additions & 3 deletions proxy-bin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "doh-auth-proxy"
description = "DNS Proxy for DoH, ODoH and Mutualized ODoH with Authorization"
version = "0.3.1"
version = "0.3.2"
authors = ["Jun Kurihara"]
homepage = "https://github.com/junkurihara/doh-auth-proxy"
repository = "https://github.com/junkurihara/doh-auth-proxy"
Expand Down Expand Up @@ -37,7 +37,7 @@ doh-auth-proxy-lib = { path = "../proxy-lib/" }
anyhow = "1.0.79"
mimalloc = { version = "*", default-features = false }
serde = { version = "1.0.195", default-features = false, features = ["derive"] }
derive_builder = "0.12.0"
derive_builder = "0.13.0"
tokio = { version = "1.35.1", default-features = false, features = [
"net",
"rt-multi-thread",
Expand All @@ -48,7 +48,7 @@ tokio = { version = "1.35.1", default-features = false, features = [
async-trait = "0.1.77"

# config
clap = { version = "4.4.14", features = ["std", "cargo", "wrap_help"] }
clap = { version = "4.4.18", features = ["std", "cargo", "wrap_help"] }
toml = { version = "0.8.8", default-features = false, features = ["parse"] }
hot_reload = "0.1.4"

Expand Down
1 change: 1 addition & 0 deletions proxy-bin/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod parse;
mod plugins;
mod target_config;
mod toml;
mod utils_dns_proto;
mod utils_verifier;

pub use {
Expand Down
13 changes: 9 additions & 4 deletions proxy-bin/src/config/target_config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{toml::ConfigToml, utils_verifier::*};
use super::{toml::ConfigToml, utils_dns_proto::parse_proto_sockaddr_str, utils_verifier::*};
use crate::{constants::*, error::*, log::*};
use async_trait::async_trait;
use doh_auth_proxy_lib::{
Expand Down Expand Up @@ -76,12 +76,17 @@ impl TryInto<ProxyConfig> for &TargetConfig {
/////////////////////////////
// bootstrap dns
if let Some(val) = &self.config_toml.bootstrap_dns {
if !val.iter().all(|v| verify_ip_addr(v).is_ok()) {
let vec_proto_sockaddr = val.iter().map(parse_proto_sockaddr_str).collect::<Vec<_>>();
if vec_proto_sockaddr.iter().any(|x| x.is_err()) {
bail!("Invalid bootstrap DNS address");
}
proxy_config.bootstrap_dns.ips = val.iter().map(|x| x.parse().unwrap()).collect()
proxy_config.bootstrap_dns = vec_proto_sockaddr
.iter()
.map(|x| x.as_ref().unwrap().clone())
.collect::<Vec<_>>()
.try_into()?;
};
info!("Bootstrap DNS: {:?}", proxy_config.bootstrap_dns.ips);
info!("Bootstrap DNS: {}", proxy_config.bootstrap_dns);

/////////////////////////////
// endpoint re-resolution period
Expand Down
95 changes: 95 additions & 0 deletions proxy-bin/src/config/utils_dns_proto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};

const PREFIX_UDP: &str = "udp://";
const PREFIX_TCP: &str = "tcp://";
const DEFAULT_DNS_PORT: u16 = 53;

/// Parse as string in the form of "<proto>://<ip_addr>:<port>", where "<proto>://" can be omitted and then it will be treated as "udp://".
/// ":<port>" can also be omitted and then it will be treated as ":53".
/// - <proto>: "udp" or "tcp"
/// - <ip_addr>: IPv4 or IPv6 address, where IPv6 address must be enclosed in square brackets like "[::1]"
/// - <port>: port number, which must be explicitly specified.
pub(crate) fn parse_proto_sockaddr_str<T: AsRef<str>>(val: T) -> anyhow::Result<(String, SocketAddr)> {
let val = val.as_ref();

// parse proto
let (proto, val_rest) = if val.starts_with(PREFIX_UDP) {
("udp", val.strip_prefix(PREFIX_UDP).unwrap())
} else if val.starts_with(PREFIX_TCP) {
("tcp", val.strip_prefix(PREFIX_TCP).unwrap())
} else {
("udp", val)
};

// parse socket address
let socket_addr = if val.contains('[') && val.contains(']') {
// ipv6
let mut split = val_rest.strip_prefix('[').unwrap().split(']').filter(|s| !s.is_empty());
let ip_part = split
.next()
.ok_or(anyhow::anyhow!("Invalid IPv6 address specified"))?
.parse::<Ipv6Addr>()?;
let port_part = if let Some(port_part) = split.next() {
anyhow::ensure!(port_part.starts_with(':'), "Invalid port number specified");
port_part.strip_prefix(':').unwrap().parse::<u16>()?
} else {
DEFAULT_DNS_PORT
};
SocketAddr::new(IpAddr::V6(ip_part), port_part)
} else {
// ipv4
let mut split = val_rest.split(':').filter(|s| !s.is_empty());
let ip_part = split
.next()
.ok_or(anyhow::anyhow!("Invalid IPv4 address specified"))?
.parse::<Ipv4Addr>()?;
let port_part = if let Some(port_part) = split.next() {
port_part.parse::<u16>()?
} else {
DEFAULT_DNS_PORT
};
SocketAddr::new(IpAddr::V4(ip_part), port_part)
};

Ok((proto.to_owned(), socket_addr))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_proto_sockaddr_str() {
let (proto, socket_addr) = parse_proto_sockaddr_str("tcp://[::1]:50053").unwrap();
assert_eq!(proto, "tcp");
assert_eq!(socket_addr, SocketAddr::from((Ipv6Addr::LOCALHOST, 50053)));

let (proto, socket_addr) = parse_proto_sockaddr_str("tcp://[::1]").unwrap();
assert_eq!(proto, "tcp");
assert_eq!(socket_addr, SocketAddr::from((Ipv6Addr::LOCALHOST, 53)));

let (proto, socket_addr) = parse_proto_sockaddr_str("[::1]:50053").unwrap();
assert_eq!(proto, "udp");
assert_eq!(socket_addr, SocketAddr::from((Ipv6Addr::LOCALHOST, 50053)));

let (proto, socket_addr) = parse_proto_sockaddr_str("[::1]").unwrap();
assert_eq!(proto, "udp");
assert_eq!(socket_addr, SocketAddr::from((Ipv6Addr::LOCALHOST, 53)));

let (proto, socket_addr) = parse_proto_sockaddr_str("udp://8.8.8.8:50053").unwrap();
assert_eq!(proto, "udp");
assert_eq!(socket_addr, SocketAddr::from(([8, 8, 8, 8], 50053)));

let (proto, socket_addr) = parse_proto_sockaddr_str("udp://8.8.8.8").unwrap();
assert_eq!(proto, "udp");
assert_eq!(socket_addr, SocketAddr::from(([8, 8, 8, 8], 53)));

let (proto, socket_addr) = parse_proto_sockaddr_str("8.8.8.8:50053").unwrap();
assert_eq!(proto, "udp");
assert_eq!(socket_addr, SocketAddr::from(([8, 8, 8, 8], 50053)));

let (proto, socket_addr) = parse_proto_sockaddr_str("8.8.8.8").unwrap();
assert_eq!(proto, "udp");
assert_eq!(socket_addr, SocketAddr::from(([8, 8, 8, 8], 53)));
}
}
1 change: 1 addition & 0 deletions proxy-bin/src/config/utils_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub(crate) fn verify_sock_addr(arg_val: &str) -> Result<(), String> {
}
}

#[allow(dead_code)]
pub(crate) fn verify_ip_addr(arg_val: &str) -> Result<(), String> {
match arg_val.parse::<IpAddr>() {
Ok(_addr) => Ok(()),
Expand Down
8 changes: 4 additions & 4 deletions proxy-lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "doh-auth-proxy-lib"
description = "DNS Proxy Library for DoH, ODoH and Mutualized ODoH with Authorization"
version = "0.3.1"
version = "0.3.2"
authors = ["Jun Kurihara"]
homepage = "https://github.com/junkurihara/doh-auth-proxy"
repository = "https://github.com/junkurihara/doh-auth-proxy"
Expand Down Expand Up @@ -59,7 +59,7 @@ hickory-proto = { version = "0.24.0", default-features = false }
data-encoding = "2.5.0"
hashlink = "0.9.0"
cedarwood = "0.4.6"
regex = "1.10.2"
regex = "1.10.3"

# network
socket2 = "0.5.5"
Expand All @@ -74,8 +74,8 @@ reqwest = { version = "0.11.23", default-features = false, features = [
url = "2.5.0"

# for bootstrap dns resolver
hickory-resolver = { version = "0.24.0", default-features = false, features = [
"tokio-runtime",
hickory-client = { version = "0.24.0", default-features = false, features = [
"dnssec",
] }

# authentication
Expand Down
Loading

0 comments on commit 5f96a96

Please sign in to comment.