Support bracketed IPv6 forwarding specs
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
This commit is contained in:
+109
-28
@@ -3951,17 +3951,14 @@ fn parse_dynamic_forwards(raw: &[String]) -> Result<Vec<DynamicForward>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_local_forward(raw: &str) -> Result<LocalForward> {
|
fn parse_local_forward(raw: &str) -> Result<LocalForward> {
|
||||||
let parts = raw.split(':').collect::<Vec<_>>();
|
let parts = split_forward_spec(raw)?;
|
||||||
let (bind_host, listen, target_host, target_port) = match parts.as_slice() {
|
let (bind_host, listen, target_host, target_port) = match parts.as_slice() {
|
||||||
[listen, target_host, target_port] => {
|
[listen, target_host, target_port] => {
|
||||||
("127.0.0.1".to_string(), *listen, *target_host, *target_port)
|
("127.0.0.1".to_string(), listen, target_host, target_port)
|
||||||
|
}
|
||||||
|
[bind_host, listen, target_host, target_port] => {
|
||||||
|
(bind_host.to_string(), listen, target_host, target_port)
|
||||||
}
|
}
|
||||||
[bind_host, listen, target_host, target_port] => (
|
|
||||||
(*bind_host).to_string(),
|
|
||||||
*listen,
|
|
||||||
*target_host,
|
|
||||||
*target_port,
|
|
||||||
),
|
|
||||||
_ => {
|
_ => {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"invalid -L {raw:?}; expected listen_port:target_host:target_port or bind_host:listen_port:target_host:target_port"
|
"invalid -L {raw:?}; expected listen_port:target_host:target_port or bind_host:listen_port:target_host:target_port"
|
||||||
@@ -3985,13 +3982,17 @@ fn parse_local_forward(raw: &str) -> Result<LocalForward> {
|
|||||||
"invalid -L {raw:?}; listen port cannot be 0"
|
"invalid -L {raw:?}; listen port cannot be 0"
|
||||||
);
|
);
|
||||||
anyhow::ensure!(
|
anyhow::ensure!(
|
||||||
target_port != 0 || matches!(target_host, FILE_STREAM_SENTINEL | EXEC_STREAM_SENTINEL),
|
target_port != 0
|
||||||
|
|| matches!(
|
||||||
|
target_host.as_str(),
|
||||||
|
FILE_STREAM_SENTINEL | EXEC_STREAM_SENTINEL
|
||||||
|
),
|
||||||
"invalid -L {raw:?}; target port cannot be 0"
|
"invalid -L {raw:?}; target port cannot be 0"
|
||||||
);
|
);
|
||||||
Ok(LocalForward {
|
Ok(LocalForward {
|
||||||
bind_host,
|
bind_host,
|
||||||
listen_port,
|
listen_port,
|
||||||
target_host: target_host.to_string(),
|
target_host: target_host.clone(),
|
||||||
target_port,
|
target_port,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -4007,10 +4008,10 @@ fn parse_remote_forward(raw: &str) -> Result<RemoteForward> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_dynamic_forward(raw: &str) -> Result<DynamicForward> {
|
fn parse_dynamic_forward(raw: &str) -> Result<DynamicForward> {
|
||||||
let parts = raw.split(':').collect::<Vec<_>>();
|
let parts = split_forward_spec(raw)?;
|
||||||
let (bind_host, listen) = match parts.as_slice() {
|
let (bind_host, listen) = match parts.as_slice() {
|
||||||
[listen] => ("127.0.0.1".to_string(), *listen),
|
[listen] => ("127.0.0.1".to_string(), listen),
|
||||||
[bind_host, listen] => ((*bind_host).to_string(), *listen),
|
[bind_host, listen] => (bind_host.to_string(), listen),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"invalid -D {raw:?}; expected listen_port or bind_host:listen_port"
|
"invalid -D {raw:?}; expected listen_port or bind_host:listen_port"
|
||||||
@@ -4033,6 +4034,45 @@ fn parse_dynamic_forward(raw: &str) -> Result<DynamicForward> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn split_forward_spec(raw: &str) -> Result<Vec<String>> {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
let mut current = String::new();
|
||||||
|
let mut bracketed = false;
|
||||||
|
let mut chars = raw.chars().peekable();
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'[' if current.is_empty() && !bracketed => {
|
||||||
|
bracketed = true;
|
||||||
|
}
|
||||||
|
']' if bracketed => {
|
||||||
|
bracketed = false;
|
||||||
|
if !matches!(chars.peek(), Some(':') | None) {
|
||||||
|
bail!(
|
||||||
|
"invalid forwarding spec {raw:?}; bracketed host must be followed by ':'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
':' if !bracketed => {
|
||||||
|
parts.push(std::mem::take(&mut current));
|
||||||
|
}
|
||||||
|
_ => current.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bracketed {
|
||||||
|
bail!("invalid forwarding spec {raw:?}; missing closing ']'");
|
||||||
|
}
|
||||||
|
parts.push(current);
|
||||||
|
Ok(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_bind_display(host: &str, port: u16) -> String {
|
||||||
|
if host.contains(':') && !host.starts_with('[') {
|
||||||
|
format!("[{host}]:{port}")
|
||||||
|
} else {
|
||||||
|
format!("{host}:{port}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn valid_forward_host(host: &str) -> bool {
|
fn valid_forward_host(host: &str) -> bool {
|
||||||
!host.is_empty()
|
!host.is_empty()
|
||||||
&& !host
|
&& !host
|
||||||
@@ -7904,7 +7944,7 @@ async fn start_local_forwards(
|
|||||||
stream_ids: Arc<AtomicU64>,
|
stream_ids: Arc<AtomicU64>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for forward in local_forwards {
|
for forward in local_forwards {
|
||||||
let bind = format!("{}:{}", forward.bind_host, forward.listen_port);
|
let bind = socket_bind_display(&forward.bind_host, forward.listen_port);
|
||||||
let listener = TcpListener::bind(&bind)
|
let listener = TcpListener::bind(&bind)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("bind local forward {bind}"))?;
|
.with_context(|| format!("bind local forward {bind}"))?;
|
||||||
@@ -7972,7 +8012,7 @@ async fn start_dynamic_forwards(
|
|||||||
stream_ids: Arc<AtomicU64>,
|
stream_ids: Arc<AtomicU64>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for forward in dynamic_forwards {
|
for forward in dynamic_forwards {
|
||||||
let bind = format!("{}:{}", forward.bind_host, forward.listen_port);
|
let bind = socket_bind_display(&forward.bind_host, forward.listen_port);
|
||||||
let listener = TcpListener::bind(&bind)
|
let listener = TcpListener::bind(&bind)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("bind dynamic forward {bind}"))?;
|
.with_context(|| format!("bind dynamic forward {bind}"))?;
|
||||||
@@ -10662,19 +10702,19 @@ mod tests {
|
|||||||
should_flush_terminal_input_after_contact, should_health_log_client_start,
|
should_flush_terminal_input_after_contact, should_health_log_client_start,
|
||||||
should_hold_during_startup_gate, should_hold_post_submit_input,
|
should_hold_during_startup_gate, should_hold_post_submit_input,
|
||||||
should_reconnect_before_input_for_local_sleep, should_repaint_idle_terminal,
|
should_reconnect_before_input_for_local_sleep, should_repaint_idle_terminal,
|
||||||
should_strip_unowned_terminal_reports, split_after_command_submit, split_trace_tokens,
|
should_strip_unowned_terminal_reports, socket_bind_display, split_after_command_submit,
|
||||||
ssh_command_target, ssh_config_uses_proxy, ssh_config_word_for_os, ssh_destination_host,
|
split_forward_spec, split_trace_tokens, ssh_command_target, ssh_config_uses_proxy,
|
||||||
ssh_username, ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports,
|
ssh_config_word_for_os, ssh_destination_host, ssh_username, ssh_with_user, startup_command,
|
||||||
strip_terminal_focus_reports, strip_unowned_terminal_reports, summarize_trace_file,
|
status_ssh_target, strip_stale_mouse_reports, strip_terminal_focus_reports,
|
||||||
summarize_trace_file_with_mode, terminal_private_mode_transition, toml_bare_key_or_quoted,
|
strip_unowned_terminal_reports, summarize_trace_file, summarize_trace_file_with_mode,
|
||||||
top_trace_events, trace_report_warnings, unix_update_script,
|
terminal_private_mode_transition, toml_bare_key_or_quoted, top_trace_events,
|
||||||
update_binary_version_for_installer, update_installer_url, update_version_status,
|
trace_report_warnings, unix_update_script, update_binary_version_for_installer,
|
||||||
upsert_managed_block, valid_forward_host, vscode_command_candidates,
|
update_installer_url, update_version_status, upsert_managed_block, valid_forward_host,
|
||||||
vscode_fallback_command, vscode_safe_alias, wake_repaint_retry_deadline,
|
vscode_command_candidates, vscode_fallback_command, vscode_safe_alias,
|
||||||
windows_command_word, windows_deferred_update_script, windows_effective_url_script,
|
wake_repaint_retry_deadline, windows_command_word, windows_deferred_update_script,
|
||||||
windows_mode_from_readonly, windows_powershell_command_candidates,
|
windows_effective_url_script, windows_mode_from_readonly,
|
||||||
windows_readonly_from_mode, windows_update_script, windows_url_reachable_script,
|
windows_powershell_command_candidates, windows_readonly_from_mode, windows_update_script,
|
||||||
windows_vt_output_mode,
|
windows_url_reachable_script, windows_vt_output_mode,
|
||||||
};
|
};
|
||||||
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
||||||
use dosh::native::EnvVar;
|
use dosh::native::EnvVar;
|
||||||
@@ -13473,6 +13513,47 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forward_parser_accepts_bracketed_ipv6_hosts() {
|
||||||
|
assert_eq!(
|
||||||
|
split_forward_spec("[::1]:8080:[2001:db8::1]:80").unwrap(),
|
||||||
|
vec!["::1", "8080", "2001:db8::1", "80"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_local_forward("[::1]:8080:[2001:db8::1]:80").unwrap(),
|
||||||
|
LocalForward {
|
||||||
|
bind_host: "::1".to_string(),
|
||||||
|
listen_port: 8080,
|
||||||
|
target_host: "2001:db8::1".to_string(),
|
||||||
|
target_port: 80,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_local_forward("8080:[::1]:80").unwrap(),
|
||||||
|
LocalForward {
|
||||||
|
bind_host: "127.0.0.1".to_string(),
|
||||||
|
listen_port: 8080,
|
||||||
|
target_host: "::1".to_string(),
|
||||||
|
target_port: 80,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_dynamic_forward("[::1]:1080").unwrap(),
|
||||||
|
DynamicForward {
|
||||||
|
bind_host: "::1".to_string(),
|
||||||
|
listen_port: 1080,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(socket_bind_display("::1", 1080), "[::1]:1080");
|
||||||
|
assert_eq!(socket_bind_display("127.0.0.1", 1080), "127.0.0.1:1080");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forward_parser_rejects_malformed_bracketed_hosts() {
|
||||||
|
assert!(parse_local_forward("[::1:8080:host:80").is_err());
|
||||||
|
assert!(parse_local_forward("[::1]8080:host:80").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn forward_command_rewrites_to_forward_only_connection() {
|
fn forward_command_rewrites_to_forward_only_connection() {
|
||||||
let args = test_args(
|
let args = test_args(
|
||||||
|
|||||||
Reference in New Issue
Block a user