Skip to content

Commit 18ba7f4

Browse files
committed
feat: allow choosing expose bind address
1 parent f290b5d commit 18ba7f4

4 files changed

Lines changed: 78 additions & 8 deletions

File tree

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,14 @@ In short: the agent cannot freely access the network; users can run sandbox comm
8282
```bash
8383
cladding expose 3000
8484
cladding expose 3000 9000
85+
cladding expose 3000 --bind-address 192.168.1.20
86+
cladding expose 3000 9000 --bind-address ::1
8587
```
8688

87-
`cladding expose` runs in the foreground. Stop it with Ctrl-C.
89+
`cladding expose` runs in the foreground and defaults to listening on
90+
`127.0.0.1`. Use `--bind-address` to select a host IP address; `0.0.0.0`
91+
or `::` listens on all IPv4 or IPv6 interfaces respectively. Stop it with
92+
Ctrl-C.
8893

8994
* Temporarily make one host-reachable TCP endpoint available on agent localhost while the project is running:
9095

@@ -215,7 +220,7 @@ cladding check # verify required paths/images
215220
cladding ps # list running cladding projects
216221
cladding run [--env KEY[=VALUE] ...] [cmd] # run a command in the agent container
217222
cladding run-with-scissors [--target nw-sandbox|fs-sandbox] [--env KEY[=VALUE] ...] [cmd] # run a command in an enabled sandbox container
218-
cladding expose <containerport> [hostport] # block while forwarding localhost hostport to agent containerport
223+
cladding expose <containerport> [hostport] [--bind-address <address>] # block while forwarding host address/port to agent containerport
219224
cladding inject <host-endpoint> [containerport] # block while forwarding agent localhost containerport to a host-reachable endpoint
220225
cladding reload-proxy # reconfigure squid after domain-list edits
221226
cladding logs [agent|proxy|nw-sandbox|fs-sandbox] [podman logs args...] # view container logs

docs/features/current-runtime-summary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The proxy bridge sidecar uses the proxy socket directories. The agent uses the p
3838

3939
## Blocking `cladding expose`
4040
- `cladding expose <container-port> [host-port]` runs in the foreground on the host.
41-
- It binds `127.0.0.1:<host-port>` and forwards through `cladding run socat ...` to `127.0.0.1:<container-port>` inside the agent container.
41+
- It binds `127.0.0.1:<host-port>` by default, or the address selected with `--bind-address`, and forwards through `cladding run socat ...` to `127.0.0.1:<container-port>` inside the agent container.
4242
- No persistent expose containers are created.
4343

4444
## Blocking `cladding inject`

src/cli/args.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use cladding::config::ExecutionConfig;
22
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
3+
use std::net::IpAddr;
34
use std::path::PathBuf;
45

56
const VERSION: &str = jj_version::jj_version!(fallback = env!("CARGO_PKG_VERSION"),);
@@ -75,6 +76,9 @@ pub(super) struct ExposeArgs {
7576
pub(super) container_port: u16,
7677
#[arg(value_name = "HOSTPORT", value_parser = clap::value_parser!(u16).range(1..=65535))]
7778
pub(super) host_port: Option<u16>,
79+
/// Host IP address on which to listen
80+
#[arg(long, value_name = "ADDRESS", default_value = "127.0.0.1")]
81+
pub(super) bind_address: IpAddr,
7882
}
7983

8084
#[derive(Debug, Args)]
@@ -223,6 +227,7 @@ mod tests {
223227
CommandSpec::Expose(args) => {
224228
assert_eq!(args.container_port, 3000);
225229
assert_eq!(args.host_port, None);
230+
assert_eq!(args.bind_address, "127.0.0.1".parse::<IpAddr>().unwrap());
226231
}
227232
other => panic!("unexpected command: {other:?}"),
228233
}
@@ -235,11 +240,46 @@ mod tests {
235240
CommandSpec::Expose(args) => {
236241
assert_eq!(args.container_port, 3000);
237242
assert_eq!(args.host_port, Some(9000));
243+
assert_eq!(args.bind_address, "127.0.0.1".parse::<IpAddr>().unwrap());
238244
}
239245
other => panic!("unexpected command: {other:?}"),
240246
}
241247
}
242248

249+
#[test]
250+
fn expose_bind_address_parses_ipv4_and_ipv6() {
251+
let ipv4 = Cli::try_parse_from([
252+
"cladding",
253+
"expose",
254+
"3000",
255+
"--bind-address",
256+
"192.168.1.20",
257+
])
258+
.expect("ipv4 bind address should parse");
259+
match ipv4.command.expect("command") {
260+
CommandSpec::Expose(args) => {
261+
assert_eq!(args.bind_address, "192.168.1.20".parse::<IpAddr>().unwrap());
262+
}
263+
other => panic!("unexpected command: {other:?}"),
264+
}
265+
266+
let ipv6 = Cli::try_parse_from(["cladding", "expose", "3000", "--bind-address", "::1"])
267+
.expect("ipv6 bind address should parse");
268+
match ipv6.command.expect("command") {
269+
CommandSpec::Expose(args) => {
270+
assert_eq!(args.bind_address, "::1".parse::<IpAddr>().unwrap());
271+
}
272+
other => panic!("unexpected command: {other:?}"),
273+
}
274+
}
275+
276+
#[test]
277+
fn expose_bind_address_rejects_interface_names() {
278+
assert!(
279+
Cli::try_parse_from(["cladding", "expose", "3000", "--bind-address", "eth0",]).is_err()
280+
);
281+
}
282+
243283
#[test]
244284
fn up_and_down_verbose_flags_parse() {
245285
let up = Cli::try_parse_from(["cladding", "up", "-v"]).expect("cli parse");

src/cli/expose.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use cladding::error::{Error, Result};
77
use cladding::fs_utils::is_executable;
88
use cladding::podman::{podman_container_exists, podman_required};
99
use std::env;
10+
use std::net::IpAddr;
1011
use std::path::Path;
1112
use std::process::Command;
1213

@@ -35,9 +36,14 @@ pub(super) fn cmd_expose(context: &Context, args: &ExposeArgs) -> Result<()> {
3536
let host_port = args.host_port.unwrap_or(args.container_port);
3637
let current_exe =
3738
env::current_exe().with_context(|| "failed to determine current executable")?;
38-
let status = build_blocking_expose_command(&current_exe, args.container_port, host_port)
39-
.status()
40-
.with_context(|| "failed to run socat")?;
39+
let status = build_blocking_expose_command(
40+
&current_exe,
41+
args.container_port,
42+
host_port,
43+
args.bind_address,
44+
)
45+
.status()
46+
.with_context(|| "failed to run socat")?;
4147

4248
cladding::podman::ensure_success(status, "socat")
4349
}
@@ -61,10 +67,15 @@ fn build_blocking_expose_command(
6167
current_exe: &Path,
6268
container_port: u16,
6369
host_port: u16,
70+
bind_address: IpAddr,
6471
) -> Command {
6572
let mut cmd = Command::new("socat");
73+
let listener = match bind_address {
74+
IpAddr::V4(_) => "TCP4-LISTEN",
75+
IpAddr::V6(_) => "TCP6-LISTEN",
76+
};
6677
cmd.arg(format!(
67-
"TCP-LISTEN:{host_port},bind=127.0.0.1,reuseaddr,fork"
78+
"{listener}:{host_port},bind={bind_address},reuseaddr,fork"
6879
));
6980
let current_exe = shell_single_quote_path(current_exe);
7081
cmd.arg(format!(
@@ -95,16 +106,30 @@ mod tests {
95106
Path::new("/tmp/cladding test/bin's/cladding"),
96107
5432,
97108
15432,
109+
"127.0.0.1".parse::<IpAddr>().unwrap(),
98110
);
99111
let args = command_args(&cmd);
100112

101113
assert_eq!(cmd.get_program().to_string_lossy(), "socat");
102114
assert_eq!(args.len(), 2);
103-
assert_eq!(args[0], "TCP-LISTEN:15432,bind=127.0.0.1,reuseaddr,fork");
115+
assert_eq!(args[0], "TCP4-LISTEN:15432,bind=127.0.0.1,reuseaddr,fork");
104116
assert_eq!(
105117
args[1],
106118
"EXEC:'/tmp/cladding test/bin'\\''s/cladding' run socat STDIO TCP\\:127.0.0.1\\:5432"
107119
);
108120
assert!(!args.iter().any(|arg| arg.starts_with("--")));
109121
}
122+
123+
#[test]
124+
fn build_blocking_expose_command_uses_ipv6_listener_for_ipv6_bind() {
125+
let cmd = build_blocking_expose_command(
126+
Path::new("/usr/local/bin/cladding"),
127+
3000,
128+
9000,
129+
"::1".parse::<IpAddr>().unwrap(),
130+
);
131+
let args = command_args(&cmd);
132+
133+
assert_eq!(args[0], "TCP6-LISTEN:9000,bind=::1,reuseaddr,fork");
134+
}
110135
}

0 commit comments

Comments
 (0)