Skip to content

Commit e776b24

Browse files
feat(agent): --advertise-domains CLI flag
Master already plumbs `tunnel_conf.advertise_domains` into the agent's `RouteAdvertise` control message and the gateway's domain-suffix routing table, but there was no way to set the list from the CLI: the field was config-file-only. This PR adds a `--advertise-domains` flag (parallel to `--advertise-routes`) on both the legacy positional `enroll` command and the structured `up` subcommand, threaded through `enroll_agent` to the persisted `TunnelConf`. An empty list from the caller preserves whatever is already in the config so admins can manage domains via the config file without losing them on re-enrollment from the CLI.
1 parent 0475494 commit e776b24

2 files changed

Lines changed: 59 additions & 11 deletions

File tree

devolutions-agent/src/enrollment.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,17 +97,28 @@ pub struct PersistedEnrollment {
9797
/// * `enrollment_token` - JWT token for enrollment
9898
/// * `agent_name` - Friendly name for this agent
9999
/// * `advertise_subnets` - List of subnets to advertise (e.g., ["10.0.0.0/8"])
100+
/// * `advertise_domains` - List of DNS suffixes to advertise (e.g.,
101+
/// ["corp.example.com"]). Empty preserves the existing config value so the
102+
/// admin can manage domains via the config file without losing them on
103+
/// re-enrollment.
100104
pub async fn enroll_agent(
101105
gateway_url: &str,
102106
enrollment_token: &str,
103107
agent_name: &str,
104108
advertise_subnets: Vec<String>,
109+
advertise_domains: Vec<String>,
105110
) -> Result<PersistedEnrollment> {
106111
// Generate key pair and CSR locally — the private key never leaves this machine.
107112
let (key_pem, csr_pem) = generate_key_and_csr(agent_name)?;
108113

109114
let enroll_response = request_enrollment(gateway_url, enrollment_token, agent_name, &csr_pem).await?;
110-
persist_enrollment_response(agent_name, advertise_subnets, enroll_response, &key_pem)
115+
persist_enrollment_response(
116+
agent_name,
117+
advertise_subnets,
118+
advertise_domains,
119+
enroll_response,
120+
&key_pem,
121+
)
111122
}
112123

113124
/// Generate an ECDSA P-256 key pair and a CSR containing the agent name as CN.
@@ -164,6 +175,7 @@ async fn request_enrollment(
164175
fn persist_enrollment_response(
165176
agent_name: &str,
166177
advertise_subnets: Vec<String>,
178+
advertise_domains: Vec<String>,
167179
EnrollResponse {
168180
agent_id,
169181
client_cert_pem,
@@ -214,17 +226,25 @@ fn persist_enrollment_response(
214226
// configured by the MSI installer or admin.
215227
let mut conf_file = config::load_conf_file_or_generate_new().context("failed to load existing configuration")?;
216228

217-
// Preserve existing domain config from previous enrollment/manual configuration.
218229
let existing_tunnel = conf_file.tunnel.as_ref();
219230

231+
// An empty `advertise_domains` from the caller means "no override" — fall
232+
// back to the value already in the config so re-enrollment from the CLI
233+
// does not silently wipe domains the admin set via the config file.
234+
let resolved_advertise_domains = if advertise_domains.is_empty() {
235+
existing_tunnel.map(|t| t.advertise_domains.clone()).unwrap_or_default()
236+
} else {
237+
advertise_domains
238+
};
239+
220240
let tunnel_conf = config::dto::TunnelConf {
221241
enabled: true,
222242
gateway_endpoint: quic_endpoint.clone(),
223243
client_cert_path: Some(client_cert_path.clone()),
224244
client_key_path: Some(client_key_path.clone()),
225245
gateway_ca_cert_path: Some(gateway_ca_path.clone()),
226246
advertise_subnets,
227-
advertise_domains: existing_tunnel.map(|t| t.advertise_domains.clone()).unwrap_or_default(),
247+
advertise_domains: resolved_advertise_domains,
228248
auto_detect_domain: existing_tunnel.map(|t| t.auto_detect_domain).unwrap_or(true),
229249
heartbeat_interval_secs: Some(60),
230250
route_advertise_interval_secs: Some(30),

devolutions-agent/src/main.rs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ struct UpCommand {
5959
enrollment_token: String,
6060
agent_name: String,
6161
advertise_subnets: Vec<String>,
62+
advertise_domains: Vec<String>,
6263
}
6364

6465
fn agent_service_main(
@@ -136,11 +137,11 @@ fn parse_required_value(args: &[String], index: &mut usize, flag: &str) -> Resul
136137
.with_context(|| format!("missing value for {flag}"))
137138
}
138139

139-
fn parse_advertise_subnets(value: &str) -> Vec<String> {
140+
fn parse_comma_separated(value: &str) -> Vec<String> {
140141
value
141142
.split(',')
142143
.map(str::trim)
143-
.filter(|subnet| !subnet.is_empty())
144+
.filter(|item| !item.is_empty())
144145
.map(ToOwned::to_owned)
145146
.collect()
146147
}
@@ -151,6 +152,7 @@ fn parse_up_command_args(args: &[String]) -> Result<UpCommand> {
151152
let mut agent_name = None;
152153
let mut enrollment_string = None;
153154
let mut advertise_subnets = Vec::new();
155+
let mut advertise_domains = Vec::new();
154156

155157
let mut index = 0;
156158
while index < args.len() {
@@ -162,7 +164,10 @@ fn parse_up_command_args(args: &[String]) -> Result<UpCommand> {
162164
"--name" | "--agent-name" => agent_name = Some(parse_required_value(args, &mut index, arg)?),
163165
"--enrollment-string" => enrollment_string = Some(parse_required_value(args, &mut index, arg)?),
164166
"--advertise-routes" | "--advertise-subnets" => {
165-
advertise_subnets.extend(parse_advertise_subnets(&parse_required_value(args, &mut index, arg)?))
167+
advertise_subnets.extend(parse_comma_separated(&parse_required_value(args, &mut index, arg)?))
168+
}
169+
"--advertise-domains" => {
170+
advertise_domains.extend(parse_comma_separated(&parse_required_value(args, &mut index, arg)?))
166171
}
167172
unexpected => bail!("unknown argument for up: {unexpected}"),
168173
}
@@ -187,6 +192,7 @@ fn parse_up_command_args(args: &[String]) -> Result<UpCommand> {
187192
enrollment_token: enrollment_token.context("missing required --token")?,
188193
agent_name: agent_name.context("missing required --name")?,
189194
advertise_subnets,
195+
advertise_domains,
190196
})
191197
}
192198

@@ -239,12 +245,10 @@ fn main() {
239245
let enrollment_token = env::args().nth(3).expect("missing enrollment token");
240246
let agent_name = env::args().nth(4).expect("missing agent name");
241247
let subnets_arg = env::args().nth(5).unwrap_or_default();
248+
let domains_arg = env::args().nth(6).unwrap_or_default();
242249

243-
let advertise_subnets: Vec<String> = if subnets_arg.is_empty() {
244-
Vec::new()
245-
} else {
246-
subnets_arg.split(',').map(|s| s.trim().to_owned()).collect()
247-
};
250+
let advertise_subnets: Vec<String> = parse_comma_separated(&subnets_arg);
251+
let advertise_domains: Vec<String> = parse_comma_separated(&domains_arg);
248252

249253
let rt = tokio::runtime::Runtime::new().expect("failed to create tokio runtime");
250254
rt.block_on(async {
@@ -253,6 +257,7 @@ fn main() {
253257
&enrollment_token,
254258
&agent_name,
255259
advertise_subnets,
260+
advertise_domains,
256261
)
257262
.await
258263
{
@@ -278,6 +283,7 @@ fn main() {
278283
&command.enrollment_token,
279284
&command.agent_name,
280285
command.advertise_subnets,
286+
command.advertise_domains,
281287
)
282288
.await
283289
});
@@ -324,10 +330,32 @@ mod tests {
324330
enrollment_token: "bootstrap-token".to_owned(),
325331
agent_name: "site-a-agent".to_owned(),
326332
advertise_subnets: vec!["10.0.0.0/8".to_owned(), "192.168.1.0/24".to_owned()],
333+
advertise_domains: vec![],
327334
}
328335
);
329336
}
330337

338+
#[test]
339+
fn parse_up_command_args_accepts_advertise_domains() {
340+
let args = vec![
341+
"--gateway".to_owned(),
342+
"https://gateway.example.com:7171".to_owned(),
343+
"--token".to_owned(),
344+
"bootstrap-token".to_owned(),
345+
"--name".to_owned(),
346+
"site-a-agent".to_owned(),
347+
"--advertise-domains".to_owned(),
348+
"corp.example.com, lab.example.com".to_owned(),
349+
];
350+
351+
let parsed = parse_up_command_args(&args).expect("parse up args");
352+
353+
assert_eq!(
354+
parsed.advertise_domains,
355+
vec!["corp.example.com".to_owned(), "lab.example.com".to_owned()]
356+
);
357+
}
358+
331359
#[test]
332360
fn parse_up_command_args_accepts_aliases() {
333361
let args = vec![

0 commit comments

Comments
 (0)