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
107pub 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}