Skip to content

Commit df8d9db

Browse files
committed
feat: support attaching token for launched process and cli launcher
1 parent 00669bf commit df8d9db

12 files changed

Lines changed: 442 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

boltapi/src/schema.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ pub struct ProcessSchema {
1010
pub cmdline: String,
1111
pub cwd: String,
1212
pub parents: Vec<ProcessParentSchema>,
13+
#[serde(skip_serializing_if = "Option::is_none", default)]
14+
pub token: Option<String>,
1315
}
1416

1517
#[derive(Serialize, Deserialize, Debug, Clone)]

boltconn/src/cli/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod clean;
33
mod request;
44
mod request_uds;
55
mod request_web;
6+
mod run;
67
mod streaming;
78

89
use crate::ProgramArgs;
@@ -201,6 +202,8 @@ pub(crate) enum MasterConnOptions {
201202
pub(crate) enum SubCommand {
202203
/// Start the main program
203204
Start(StartOptions),
205+
/// Run a command with a tracking token
206+
Run(run::RunOptions),
204207
/// Reload configurations
205208
Reload,
206209
/// Validate configurations
@@ -262,6 +265,10 @@ pub(crate) async fn controller_main(args: ProgramArgs) -> ! {
262265
#[cfg(windows)]
263266
let default_uds_path = Some(r"\\.\pipe\boltconn".to_string());
264267
match args.cmd {
268+
SubCommand::Run(opts) => {
269+
let code = run::run_with_token(opts);
270+
exit(code as i32);
271+
}
265272
SubCommand::Generate(GenerateOptions::Init(init)) => {
266273
fn create(init: InitOptions) -> anyhow::Result<()> {
267274
let (config, data, _) =
@@ -442,6 +449,7 @@ pub(crate) async fn controller_main(args: ProgramArgs) -> ! {
442449
DnsOptions::Mapping { fake_ip } => requester.fake_ip_to_real(fake_ip).await,
443450
},
444451
SubCommand::Start(_)
452+
| SubCommand::Run(_)
445453
| SubCommand::Generate(_)
446454
| SubCommand::Clean
447455
| SubCommand::Log

boltconn/src/cli/run.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use crate::platform::process::validate_and_encode_token;
2+
use clap::Args;
3+
4+
#[derive(Debug, Args)]
5+
pub(crate) struct RunOptions {
6+
/// Token string to assign to the launched process (must be non-empty and
7+
/// base64-encode to at most 19 characters to satisfy the macOS shm name limit)
8+
#[arg(short = 't', long = "token")]
9+
pub token: String,
10+
/// Command and arguments to execute
11+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
12+
pub command: Vec<String>,
13+
}
14+
15+
/// Set up the token and exec the command. Returns the child's exit code (or -1 on error).
16+
pub(crate) fn run_with_token(opts: RunOptions) -> i32 {
17+
let encoded = match validate_and_encode_token(&opts.token) {
18+
Ok(e) => e,
19+
Err(msg) => {
20+
eprintln!("boltconn run: invalid token: {}", msg);
21+
return 1;
22+
}
23+
};
24+
25+
#[cfg(unix)]
26+
if let Err(e) = crate::platform::process::setup_token_fd(&encoded) {
27+
eprintln!("boltconn run: failed to set up token fd: {}", e);
28+
return 1;
29+
}
30+
31+
#[cfg(target_os = "windows")]
32+
crate::platform::process::setup_token_env(&encoded);
33+
34+
let mut iter = opts.command.into_iter();
35+
let program = match iter.next() {
36+
Some(p) => p,
37+
None => {
38+
eprintln!("boltconn run: no command specified");
39+
return 1;
40+
}
41+
};
42+
let args: Vec<String> = iter.collect();
43+
44+
match std::process::Command::new(&program).args(&args).status() {
45+
Ok(status) => status.code().unwrap_or(1),
46+
Err(e) => {
47+
eprintln!("boltconn run: failed to execute '{}': {}", program, e);
48+
1
49+
}
50+
}
51+
}

boltconn/src/config/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,9 @@ fn test_raw_root_cfg() {
273273
}
274274

275275
#[test]
276-
fn test_dispatching_config_process_info_depth_defaults_to_one() {
276+
fn test_dispatching_config_process_info_depth_defaults_to_unlimited() {
277277
let config: DispatchingConfig = serde_yaml::from_str("{}").unwrap();
278-
assert_eq!(config.process_info_depth, ProcessInfoDepth::Limited(1));
278+
assert_eq!(config.process_info_depth, ProcessInfoDepth::Unlimited);
279279
}
280280

281281
#[test]

boltconn/src/external/controller.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ impl Controller {
172172
cmdline: info.cmdline.clone(),
173173
cwd: info.cwd.clone(),
174174
parents,
175+
token: info.token.clone(),
175176
}
176177
}
177178

boltconn/src/instrument/action.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ struct InstrumentContext<'a> {
9898
time_hms_ms: String,
9999
time_datetime: String,
100100
time_datetime_ms: String,
101+
process_token: String,
101102
process_parent_all_json: String,
102103
}
103104

@@ -131,6 +132,11 @@ impl<'a> InstrumentContext<'a> {
131132
time_hms_ms: now.format("%H:%M:%S%.3f").to_string(),
132133
time_datetime: now.format("%Y-%m-%d %H:%M:%S").to_string(),
133134
time_datetime_ms: now.format("%Y-%m-%d %H:%M:%S%.3f").to_string(),
135+
process_token: info
136+
.process_info
137+
.as_ref()
138+
.and_then(|p| p.token.clone())
139+
.unwrap_or_else(|| NA_STR.to_string()),
134140
process_parent_all_json: Self::serialize_all_parents(info.process_info.as_ref()),
135141
}
136142
}
@@ -275,6 +281,7 @@ impl interpolator::Context for InstrumentContext<'_> {
275281
.as_ref()
276282
.map_or_else(Self::na, |info| Formattable::display(&info.pid)),
277283
),
284+
"process.token" => Some(Formattable::display(&self.process_token)),
278285
"process.parent.pid" => self.process_parent_field(0, "pid"),
279286
"process.parent.name" => self.process_parent_field(0, "name"),
280287
"process.parent.path" => self.process_parent_field(0, "path"),
@@ -295,7 +302,7 @@ fn test_instrument_formatting() {
295302
local_ip: {ip.local}, conn_type: {conn.type}, \
296303
inbound_type: {inbound.type}, inbound_port: {inbound.port}, inbound_user: {inbound.user}, \
297304
process_name: {process.name}, process_cmdline: {process.cmdline}, process_path: {process.path}, \
298-
process_pid: {process.pid}, process_parent_pid: {process.parent.pid}, \
305+
process_pid: {process.pid}, process_token: {process.token}, process_parent_pid: {process.parent.pid}, \
299306
process_parent_name: {process.parent.name}, process_parent_path: {process.parent.path}, \
300307
process_parent_cmdline: {process.parent.cmdline}, process_parent_indexed_name: {process.parents.0.name}, \
301308
process_parent_all_json: {process.parent.all.json}, \
@@ -324,7 +331,7 @@ time: {time.hms_ms}";
324331
local_ip: N/A, conn_type: tcp, \
325332
inbound_type: tun, inbound_port: N/A, inbound_user: N/A, \
326333
process_name: N/A, process_cmdline: N/A, process_path: N/A, \
327-
process_pid: N/A, process_parent_pid: N/A, \
334+
process_pid: N/A, process_token: N/A, process_parent_pid: N/A, \
328335
process_parent_name: N/A, process_parent_path: N/A, \
329336
process_parent_cmdline: N/A, process_parent_indexed_name: N/A, \
330337
process_parent_all_json: [], "
@@ -340,6 +347,7 @@ fn mock_process(pid: i32, name: &str, parent: ParentProcess) -> ProcessInfo {
340347
name: name.to_string(),
341348
cmdline: format!("{name} --serve"),
342349
cwd: format!("/tmp/{name}"),
350+
token: None,
343351
}
344352
}
345353

@@ -436,3 +444,24 @@ fn test_instrument_template_validation_accepts_parent_json() {
436444
let template = "ancestor={process.parent.all.json}";
437445
assert!(FormattingObject::new(template.to_string()).is_ok());
438446
}
447+
448+
#[test]
449+
fn test_instrument_formatting_process_token() {
450+
// token absent → N/A
451+
let info = mock_conn_info(None);
452+
let rendered = FormattingObject::format_inner("token={process.token}", &info).unwrap();
453+
assert_eq!(rendered, "token=N/A");
454+
455+
// process present but no token → N/A
456+
let process_no_token = mock_process(42, "curl", ParentProcess::None);
457+
let info = mock_conn_info(Some(process_no_token));
458+
let rendered = FormattingObject::format_inner("token={process.token}", &info).unwrap();
459+
assert_eq!(rendered, "token=N/A");
460+
461+
// process present with token → token value
462+
let mut process_with_token = mock_process(42, "curl", ParentProcess::None);
463+
process_with_token.token = Some("python".to_string());
464+
let info = mock_conn_info(Some(process_with_token));
465+
let rendered = FormattingObject::format_inner("token={process.token}", &info).unwrap();
466+
assert_eq!(rendered, "token=python");
467+
}

boltconn/src/platform/process/linux.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ pub fn get_process_info(pid: i32, depth: ProcessInfoDepth) -> Option<ProcessInfo
324324
name,
325325
cmdline,
326326
cwd,
327+
token: super::token::get_token_for_pid(pid),
327328
})
328329
}
329330

boltconn/src/platform/process/macos.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ pub fn get_process_info(pid: i32, depth: ProcessInfoDepth) -> Option<ProcessInfo
138138
name,
139139
cmdline,
140140
cwd,
141+
token: super::token::get_token_for_pid(pid),
141142
})
142143
}
143144

boltconn/src/platform/process/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ mod windows;
1616
#[cfg(target_os = "windows")]
1717
pub use windows::*;
1818

19+
mod token;
20+
#[cfg(target_os = "windows")]
21+
pub use token::setup_token_env;
22+
#[cfg(unix)]
23+
pub use token::setup_token_fd;
24+
pub use token::validate_and_encode_token;
25+
1926
use serde::de::{self, Visitor};
2027
use serde::{Deserialize, Deserializer, Serialize, Serializer};
2128

@@ -133,6 +140,8 @@ pub struct ProcessInfo {
133140
pub name: String,
134141
pub cmdline: String,
135142
pub cwd: String,
143+
/// Bolt token assigned at launch via `boltconn run`, if present.
144+
pub token: Option<String>,
136145
}
137146

138147
impl ProcessInfo {

0 commit comments

Comments
 (0)