doh_client/cmd/
app.rs

1use std::net::SocketAddr;
2
3use clap::value_parser;
4use clap::{crate_authors, crate_description, crate_version, Arg, ArgAction, Command};
5
6const ABOUT: &str =
7    "Open a local UDP (DNS) port and forward DNS queries to a remote HTTP/2.0 server.\n\
8    By default, the client will connect to the Cloudflare DNS service.\n\
9    This binary uses the env_logger as logger implementations. \n\
10    See https://github.com/sebasmagri/env_logger/";
11
12const AFTER_HELP: &str =
13    "CAUTION: If a domain name is used for a <Addr/Domain:Port> value instead of an IP address \
14    the system resolver will be used to resolve the IP address of the domain name. If the \
15    `doh-client` is configured as system resolver, then it will NOT WORK. It is recommended to \
16    always use an IP address for <Addr/Domain:Port> values.\n";
17
18#[cfg(any(feature = "socks5", feature = "http-proxy"))]
19fn proxy_args(app: Command) -> Command {
20    use clap::ArgAction;
21
22    let mut proxy_host = Arg::new("proxy-host")
23        .long("proxy-host")
24        .action(ArgAction::Set)
25        .value_name("Addr/Domain:Port")
26        .required(false)
27        .requires("proxy-scheme");
28
29    let mut proxy_scheme = Arg::new("proxy-scheme")
30        .long("proxy-scheme")
31        .action(ArgAction::Set)
32        .help("The protocol of the proxy")
33        .required(false)
34        .requires("proxy-host");
35
36    if cfg!(all(feature = "socks5", feature = "http-proxy")) {
37        proxy_host = proxy_host.help("Socks5 or HTTP CONNECT proxy host (see below)");
38        proxy_scheme = proxy_scheme.value_parser(["socks5", "socks5h", "http", "https"]);
39    } else if cfg!(all(feature = "socks5", not(feature = "http-proxy"))) {
40        proxy_host = proxy_host.help("Socks5 proxy host (see below)");
41        proxy_scheme = proxy_scheme.value_parser(["socks5", "socks5h"]);
42    } else {
43        proxy_host = proxy_host.help("HTTP CONNECT proxy host (see below)");
44        proxy_scheme = proxy_scheme.value_parser(["http", "https"]);
45    }
46
47    let command = app.arg(proxy_host).arg(proxy_scheme).arg(
48        Arg::new("proxy-credentials")
49            .long("proxy-credentials")
50            .action(ArgAction::Set)
51            .value_name("Username:Password")
52            .help("The credentials for the proxy")
53            .requires_all(&["proxy-host", "proxy-scheme"][..]),
54    );
55
56    if cfg!(feature = "http-proxy") {
57        let arg = Arg::new("proxy-https-cafile")
58            .value_name("CAFILE")
59            .long("proxy-https-cafile")
60            .action(ArgAction::Set);
61        let arg = if cfg!(feature = "native-certs") {
62            arg.help(
63                "The path to the pem file, which contains the trusted CA \
64                      certificates for the https proxy\n\
65                      If no path is given then the platform's native certificate store \
66                      will be used",
67            )
68            .required(false)
69        } else {
70            arg.help(
71                "The path to the pem file, which contains the trusted CA \
72                      certificates for the https proxy",
73            )
74            .required_if_eq("proxy-scheme", "https")
75        };
76        command.arg(arg).arg(
77            Arg::new("proxy-https-domain")
78                .action(ArgAction::Set)
79                .value_name("Domain")
80                .long("proxy-https-domain")
81                .help("The domain name of the https proxy")
82                .required_if_eq("proxy-scheme", "https"),
83        )
84    } else {
85        command
86    }
87}
88
89fn cafile(command: Command) -> Command {
90    let arg = Arg::new("cafile")
91        .action(ArgAction::Set)
92        .value_name("CAFILE");
93    let arg = if cfg!(feature = "native-certs") {
94        arg.help(
95            "The path to the pem file, which contains the trusted CA certificates\n\
96                  If no path is given then the platform's native certificate store will be \
97                  used",
98        )
99        .required(false)
100    } else {
101        arg.help("The path to the pem file, which contains the trusted CA certificates")
102            .required(true)
103    };
104    command.arg(arg)
105}
106
107/// Get the `clap::App` object for the argument parsing.
108pub fn get_command() -> Command {
109    let command = Command::new(crate_description!())
110        .version(crate_version!())
111        .author(crate_authors!())
112        .about(ABOUT)
113        .after_help(AFTER_HELP)
114        .arg(
115            Arg::new("listen-addr")
116                .value_parser(value_parser!(SocketAddr))
117                .short('l')
118                .long("listen-addr")
119                .conflicts_with("listen-activation")
120                .action(ArgAction::Set)
121                .value_name("Addr:Port")
122                .help("Listen address [default: 127.0.0.1:53]")
123                .required(false),
124        )
125        .arg(
126            Arg::new("listen-activation")
127                .long("listen-activation")
128                .action(ArgAction::SetTrue)
129                .conflicts_with("listen-addr")
130                .help(
131                    "Use file descriptor 3 under Unix as UDP socket or launch_activate_socket() \
132                    under Mac OS",
133                )
134                .required(false),
135        )
136        .arg(
137            Arg::new("remote-host")
138                .short('r')
139                .long("remote-host")
140                .action(ArgAction::Set)
141                .value_name("Addr/Domain:Port")
142                .help("Remote address/domain to the DOH server (see below)")
143                .default_value("1.1.1.1:443")
144                .required(false),
145        )
146        .arg(
147            Arg::new("domain")
148                .short('d')
149                .long("domain")
150                .action(ArgAction::Set)
151                .value_name("Domain")
152                .help("The domain name of the remote server")
153                .default_value("cloudflare-dns.com")
154                .required(false),
155        )
156        .arg(
157            Arg::new("retries")
158                .value_parser(value_parser!(u32))
159                .action(ArgAction::Set)
160                .long("retries")
161                .value_name("UNSIGNED INT")
162                .help("The number of retries to connect to the remote server")
163                .default_value("3")
164                .required(false),
165        )
166        .arg(
167            Arg::new("timeout")
168                .value_parser(value_parser!(u64))
169                .action(ArgAction::Set)
170                .short('t')
171                .long("timeout")
172                .value_name("UNSIGNED LONG")
173                .help(
174                    "The time in seconds after that the connection would be closed if no response \
175                    is received from the server",
176                )
177                .default_value("2")
178                .required(false),
179        )
180        .arg(
181            Arg::new("path")
182                .short('p')
183                .long("path")
184                .action(ArgAction::Set)
185                .value_name("STRING")
186                .help("The path of the URI")
187                .default_value("dns-query")
188                .required(false),
189        )
190        .arg(
191            Arg::new("get")
192                .short('g')
193                .long("get")
194                .help("Use the GET method for the HTTP/2.0 request")
195                .action(ArgAction::SetTrue)
196                .required(false),
197        )
198        .arg(
199            Arg::new("cache-size")
200                .value_parser(value_parser!(usize))
201                .long("cache-size")
202                .short('c')
203                .action(ArgAction::Set)
204                .value_name("UNSIGNED LONG")
205                .help(
206                    "The size of the private HTTP cache\n\
207                    If the size is 0 then the private HTTP cache is not used \
208                    (ignores cache-control)",
209                )
210                .default_value("1024")
211                .required(false),
212        )
213        .arg(
214            Arg::new("cache-fallback")
215                .action(ArgAction::SetTrue)
216                .long("cache-fallback")
217                .help("Use expired cache entries if no response is received from the server")
218                .required(false),
219        )
220        .arg(
221            Arg::new("client-auth-certs")
222                .long("client-auth-certs")
223                .action(ArgAction::Set)
224                .value_name("CERTSFILE")
225                .help(
226                    "The path to the pem file, which contains the certificates for the client \
227                      authentication",
228                )
229                .required(false)
230                .requires("client-auth-key"),
231        )
232        .arg(
233            Arg::new("client-auth-key")
234                .long("client-auth-key")
235                .action(ArgAction::Set)
236                .value_name("KEYFILE")
237                .help(
238                    "The path to the pem file, which contains the key for the client \
239                      authentication",
240                )
241                .required(false)
242                .requires("client-auth-certs"),
243        );
244
245    let command = cafile(command);
246
247    #[cfg(any(feature = "socks5", feature = "http-proxy"))]
248    let command = proxy_args(command);
249
250    command
251}