Compare commits

..

2 Commits

Author SHA1 Message Date
xiuting.xu
ff711a2fe1 内存优化 2026-05-12 16:45:56 +08:00
xiuting.xu
4c6b441753 优化部署 2026-05-12 15:23:09 +08:00
24 changed files with 417 additions and 157 deletions

View File

@ -3,6 +3,10 @@ name = "rpki"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bin]]
name = "rpki_rtr"
path = "src/main_rtr.rs"
[dependencies] [dependencies]
der-parser = "10.0.0" der-parser = "10.0.0"
hex = "0.4.3" hex = "0.4.3"

21
deploy/client/.env Normal file
View File

@ -0,0 +1,21 @@
# Target RTR server address for client compose files
# TCP example: 10.0.0.12:323
# TLS example: rpki.example.com:324
# SSH example: 10.0.0.12:22
RPKI_RTR_SERVER_ADDR=rpki-rtr-tcp:323
# RTR protocol version used as client command second argument (supported: 0,1,2)
RPKI_RTR_PROTOCOL_VERSION=2
# TLS server name used by --server-name in TLS mode
# Must match server certificate SAN dNSName.
RPKI_RTR_TLS_SERVER_NAME=localhost
# SSH mode examples:
# RPKI_RTR_SERVER_ADDR=10.0.0.12:2222
# RPKI_RTR_CLIENT_KEYS_VOLUME=../../certs:/app/certs:ro
# RPKI_RTR_CLIENT_KEY_PATH=/app/certs/rtr-client.key
# RPKI_RTR_SSH_SERVER_PUBKEY_PATH=/app/certs/ssh_host_rsa_key.pub
# RPKI_RTR_SSH_USERNAME=rpki-rtr
# RPKI_RTR_SSH_PASSWORD=your-password

View File

@ -3,7 +3,7 @@ version: "3.9"
services: services:
rtr-client-1: rtr-client-1:
image: rpki-rtr-debug-client:latest image: rpki-rtr-debug-client:latest
command: ["rpki-rtr-tcp:323", "2", "reset", "--keep-after-error", "--summary-only"] command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes: volumes:
- ../../logs/client:/app/logs - ../../logs/client:/app/logs
restart: no restart: no
@ -12,7 +12,7 @@ services:
rtr-client-2: rtr-client-2:
image: rpki-rtr-debug-client:latest image: rpki-rtr-debug-client:latest
command: ["rpki-rtr-tcp:323", "2", "reset", "--keep-after-error", "--summary-only"] command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes: volumes:
- ../../logs/client:/app/logs - ../../logs/client:/app/logs
restart: no restart: no
@ -21,7 +21,7 @@ services:
rtr-client-3: rtr-client-3:
image: rpki-rtr-debug-client:latest image: rpki-rtr-debug-client:latest
command: ["rpki-rtr-tcp:323", "2", "reset", "--keep-after-error", "--summary-only"] command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes: volumes:
- ../../logs/client:/app/logs - ../../logs/client:/app/logs
restart: no restart: no
@ -30,7 +30,7 @@ services:
rtr-client-4: rtr-client-4:
image: rpki-rtr-debug-client:latest image: rpki-rtr-debug-client:latest
command: ["rpki-rtr-tcp:323", "2", "reset", "--keep-after-error", "--summary-only"] command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes: volumes:
- ../../logs/client:/app/logs - ../../logs/client:/app/logs
restart: no restart: no
@ -39,7 +39,7 @@ services:
rtr-client-5: rtr-client-5:
image: rpki-rtr-debug-client:latest image: rpki-rtr-debug-client:latest
command: ["rpki-rtr-tcp:323", "2", "reset", "--keep-after-error", "--summary-only"] command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes: volumes:
- ../../logs/client:/app/logs - ../../logs/client:/app/logs
restart: no restart: no

View File

@ -0,0 +1,36 @@
version: "3.9"
services:
rtr-debug-client:
build:
context: ../..
dockerfile: deploy/client/Dockerfile
image: rpki-rtr-debug-client:latest
command:
[
"${RPKI_RTR_SERVER_ADDR:-rpki-rtr-ssh:22}",
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
"reset",
"--ssh",
"--ssh-user",
"${RPKI_RTR_SSH_USERNAME:-rpki-rtr}",
"--ssh-password",
"${RPKI_RTR_SSH_PASSWORD}",
"--ssh-server-key",
"${RPKI_RTR_SSH_SERVER_PUBKEY_PATH:-/app/certs/ssh_host_rsa_key.pub}",
"--keep-after-error",
"--summary-only"
]
volumes:
- ${RPKI_RTR_CLIENT_KEYS_VOLUME:-../../certs:/app/certs:ro}
- ../../logs/client:/app/logs
restart: no
stdin_open: true
tty: true
networks:
- rpki_net
networks:
rpki_net:
name: rpki_net
driver: bridge

View File

@ -8,21 +8,21 @@ services:
image: rpki-rtr-debug-client:latest image: rpki-rtr-debug-client:latest
command: command:
[ [
"rpki-rtr-ssh:${RPKI_RTR_SSH_PORT:-22}", "${RPKI_RTR_SERVER_ADDR:-rpki-rtr-ssh:22}",
"2", "${RPKI_RTR_PROTOCOL_VERSION:-2}",
"reset", "reset",
"--ssh", "--ssh",
"--ssh-user", "--ssh-user",
"rpki-rtr", "${RPKI_RTR_SSH_USERNAME:-rpki-rtr}",
"--ssh-key", "--ssh-key",
"/app/certs/rtr-client.key", "${RPKI_RTR_CLIENT_KEY_PATH:-/app/certs/rtr-client.key}",
"--ssh-server-key", "--ssh-server-key",
"/app/certs/ssh_host_rsa_key.pub", "${RPKI_RTR_SSH_SERVER_PUBKEY_PATH:-/app/certs/ssh_host_rsa_key.pub}",
"--keep-after-error", "--keep-after-error",
"--summary-only" "--summary-only"
] ]
volumes: volumes:
- ../../certs:/app/certs:ro - ${RPKI_RTR_CLIENT_KEYS_VOLUME:-../../certs:/app/certs:ro}
- ../../logs/client:/app/logs - ../../logs/client:/app/logs
restart: no restart: no
stdin_open: true stdin_open: true

View File

@ -4,7 +4,7 @@ services:
context: ../.. context: ../..
dockerfile: deploy/client/Dockerfile dockerfile: deploy/client/Dockerfile
image: rpki-rtr-debug-client:latest image: rpki-rtr-debug-client:latest
command: ["rpki-rtr-tcp:323", "2", "reset", "--keep-after-error", "--summary-only"] command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes: volumes:
- ../../logs/client:/app/logs - ../../logs/client:/app/logs
restart: no restart: no

View File

@ -8,14 +8,14 @@ services:
image: rpki-rtr-debug-client:latest image: rpki-rtr-debug-client:latest
command: command:
[ [
"rpki-rtr-tls:324", "${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tls:324}",
"2", "${RPKI_RTR_PROTOCOL_VERSION:-2}",
"reset", "reset",
"--tls", "--tls",
"--ca-cert", "--ca-cert",
"/app/certs/client-ca.crt", "/app/certs/client-ca.crt",
"--server-name", "--server-name",
"localhost", "${RPKI_RTR_TLS_SERVER_NAME:-localhost}",
"--client-cert", "--client-cert",
"/app/certs/client-good.crt", "/app/certs/client-good.crt",
"--client-key", "--client-key",

View File

@ -4,7 +4,7 @@ services:
context: ../.. context: ../..
dockerfile: deploy/client/Dockerfile dockerfile: deploy/client/Dockerfile
image: rpki-rtr-debug-client:latest image: rpki-rtr-debug-client:latest
command: ["rpki-rtr-tcp:323", "2", "reset", "--keep-after-error", "--summary-only"] command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes: volumes:
- ../../logs/client:/app/logs - ../../logs/client:/app/logs
restart: no restart: no

8
deploy/server/.env Normal file
View File

@ -0,0 +1,8 @@
# Host directory containing CCR files to mount into the server container.
RPKI_RTR_CCR_HOST_DIR=../../data
# In-container directory used by rpki_rtr as CCR input directory.
RPKI_RTR_CCR_DIR=/app/data
# Max retained delta count in RTR cache.
RPKI_RTR_MAX_DELTA=10

View File

@ -32,7 +32,7 @@ RUN apt-get update \
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY src ./src COPY src ./src
RUN cargo build --release --bin rpki RUN cargo build --release --bin rpki_rtr
FROM debian:bookworm-slim AS runtime FROM debian:bookworm-slim AS runtime
@ -60,7 +60,7 @@ RUN apt-get update \
WORKDIR /app WORKDIR /app
COPY --from=builder /build/target/release/rpki /usr/local/bin/rpki COPY --from=builder /build/target/release/rpki_rtr /usr/local/bin/rpki_rtr
COPY --chmod=755 deploy/server/entrypoint.sh /usr/local/bin/rpki-rtr-entrypoint.sh COPY --chmod=755 deploy/server/entrypoint.sh /usr/local/bin/rpki-rtr-entrypoint.sh
RUN mkdir -p /app/data /app/rtr-db /app/certs /app/slurm /app/logs RUN mkdir -p /app/data /app/rtr-db /app/certs /app/slurm /app/logs

View File

@ -10,29 +10,33 @@ services:
restart: no restart: no
ports: ports:
- "323:323" - "323:323"
- "${RPKI_RTR_SSH_HOST_PORT:-2222}:22" - "${RPKI_RTR_SSH_HOST_PORT:-2222}:${RPKI_RTR_SSH_CONTAINER_PORT:-22}"
environment: environment:
RPKI_RTR_ENABLE_TLS: "false" RPKI_RTR_ENABLE_TLS: "false"
RPKI_RTR_ENABLE_SSH: "true" RPKI_RTR_ENABLE_SSH: "true"
RPKI_RTR_TCP_ADDR: "0.0.0.0:323" RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
RPKI_RTR_SSH_ADDR: "0.0.0.0:22" RPKI_RTR_SSH_ADDR: "0.0.0.0:${RPKI_RTR_SSH_CONTAINER_PORT:-22}"
RPKI_RTR_SSH_HOST_KEY_PATH: "/app/certs/ssh_host_rsa_key" RPKI_RTR_SSH_HOST_KEY_PATH: "${RPKI_RTR_SSH_HOST_KEY_PATH:-/host-ssh/ssh_host_ed25519_key}"
RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH: "/app/certs/rtr-authorized_keys" RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH: "${RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH:-/app/certs/rtr-authorized_keys}"
RPKI_RTR_SSH_USERNAME: "rpki-rtr" RPKI_RTR_SSH_USERNAME: "${RPKI_RTR_SSH_USERNAME:-rpki-rtr}"
RPKI_RTR_SSH_SUBSYSTEM_NAME: "rpki-rtr" RPKI_RTR_SSH_SUBSYSTEM_NAME: "${RPKI_RTR_SSH_SUBSYSTEM_NAME:-rpki-rtr}"
# SSH auth mode: key | password | both
RPKI_RTR_SSH_AUTH_MODE: "${RPKI_RTR_SSH_AUTH_MODE:-key}"
# Optional: enable password authentication in addition to publickey # Optional: enable password authentication in addition to publickey
# RPKI_RTR_SSH_PASSWORD: "test-password" # RPKI_RTR_SSH_PASSWORD: "test-password"
RPKI_RTR_DB_PATH: "/app/rtr-db" RPKI_RTR_DB_PATH: "/app/rtr-db"
RPKI_RTR_CCR_DIR: "/app/data" RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
RPKI_RTR_SLURM_DIR: "/app/slurm" RPKI_RTR_SLURM_DIR: "/app/slurm"
RPKI_RTR_STRICT_CCR_VALIDATION: "false" RPKI_RTR_STRICT_CCR_VALIDATION: "false"
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300" RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300"
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128" RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128"
RUST_LOG: "info" RUST_LOG: "info"
volumes: volumes:
- ../../data:/app/data:ro - ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
- ../../rtr-db:/app/rtr-db - ../../rtr-db:/app/rtr-db
- ../../data:/app/slurm:ro - ../../data:/app/slurm:ro
- ${RPKI_RTR_SSH_KEYS_VOLUME:-/etc/ssh:/host-ssh:ro}
- ../../certs:/app/certs:ro - ../../certs:/app/certs:ro
- ../../logs/server:/app/logs - ../../logs/server:/app/logs
networks: networks:

View File

@ -15,15 +15,16 @@ services:
RPKI_RTR_ENABLE_SSH: "false" RPKI_RTR_ENABLE_SSH: "false"
RPKI_RTR_TCP_ADDR: "0.0.0.0:323" RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
RPKI_RTR_DB_PATH: "/app/rtr-db" RPKI_RTR_DB_PATH: "/app/rtr-db"
RPKI_RTR_CCR_DIR: "/app/data" RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
RPKI_RTR_SLURM_DIR: "/app/slurm" RPKI_RTR_SLURM_DIR: "/app/slurm"
RPKI_RTR_STRICT_CCR_VALIDATION: "false" RPKI_RTR_STRICT_CCR_VALIDATION: "false"
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "60" RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "60"
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
RPKI_RTR_MAX_CONNECTIONS: "100000" RPKI_RTR_MAX_CONNECTIONS: "100000"
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128" RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128"
RUST_LOG: "info" RUST_LOG: "info"
volumes: volumes:
- ../../data:/app/data:ro - ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
- ../../rtr-db:/app/rtr-db - ../../rtr-db:/app/rtr-db
- ../../data:/app/slurm:ro - ../../data:/app/slurm:ro
- ../../logs/server:/app/logs - ../../logs/server:/app/logs

View File

@ -21,14 +21,15 @@ services:
RPKI_RTR_TLS_CLIENT_CA_PATH: "/app/certs/client-ca.crt" RPKI_RTR_TLS_CLIENT_CA_PATH: "/app/certs/client-ca.crt"
RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH: "false" RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH: "false"
RPKI_RTR_DB_PATH: "/app/rtr-db" RPKI_RTR_DB_PATH: "/app/rtr-db"
RPKI_RTR_CCR_DIR: "/app/data" RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
RPKI_RTR_SLURM_DIR: "/app/slurm" RPKI_RTR_SLURM_DIR: "/app/slurm"
RPKI_RTR_STRICT_CCR_VALIDATION: "false" RPKI_RTR_STRICT_CCR_VALIDATION: "false"
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300" RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300"
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128" RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128"
RUST_LOG: "info" RUST_LOG: "info"
volumes: volumes:
- ../../data:/app/data:ro - ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
- ../../rtr-db:/app/rtr-db - ../../rtr-db:/app/rtr-db
- ../../data:/app/slurm:ro - ../../data:/app/slurm:ro
- ../../tests/fixtures/tls:/app/certs:ro - ../../tests/fixtures/tls:/app/certs:ro

View File

@ -18,10 +18,11 @@ services:
RPKI_RTR_TCP_ADDR: "0.0.0.0:323" RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
RPKI_RTR_TLS_ADDR: "0.0.0.0:324" RPKI_RTR_TLS_ADDR: "0.0.0.0:324"
RPKI_RTR_DB_PATH: "/app/rtr-db" RPKI_RTR_DB_PATH: "/app/rtr-db"
RPKI_RTR_CCR_DIR: "/app/data" RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
RPKI_RTR_SLURM_DIR: "/app/slurm" RPKI_RTR_SLURM_DIR: "/app/slurm"
RPKI_RTR_STRICT_CCR_VALIDATION: "false" RPKI_RTR_STRICT_CCR_VALIDATION: "false"
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300" RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300"
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128" RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128"
RUST_LOG: "info" RUST_LOG: "info"
# SSH mode example: # SSH mode example:
@ -35,7 +36,7 @@ services:
# Optional: enable password auth in addition to publickey # Optional: enable password auth in addition to publickey
# RPKI_RTR_SSH_PASSWORD: "test-password" # RPKI_RTR_SSH_PASSWORD: "test-password"
volumes: volumes:
- ../../data:/app/data:ro - ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
- ../../rtr-db:/app/rtr-db - ../../rtr-db:/app/rtr-db
- ../../data:/app/slurm:ro - ../../data:/app/slurm:ro
- ../../logs/server:/app/logs - ../../logs/server:/app/logs

View File

@ -7,4 +7,4 @@ log_name="${HOSTNAME:-rpki-rtr}"
stdout_log="/app/logs/${log_name}.stdout.log" stdout_log="/app/logs/${log_name}.stdout.log"
stderr_log="/app/logs/${log_name}.stderr.log" stderr_log="/app/logs/${log_name}.stderr.log"
exec /usr/local/bin/rpki "$@" >>"$stdout_log" 2>>"$stderr_log" exec /usr/local/bin/rpki_rtr "$@" >>"$stdout_log" 2>>"$stderr_log"

View File

@ -148,3 +148,24 @@ SSH 参数:
- `keep-after-error` - `keep-after-error`
- `output` / `output verbose` / `output summary` - `output` / `output verbose` / `output summary`
- `quit` - `quit`
## SSH Password Auth (Added)
`rtr_debug_client` now supports password auth in SSH mode.
- Use exactly one auth option in SSH mode:
- `--ssh-key <path>`
- `--ssh-password <value>`
- Host key verification is still required:
- `--ssh-known-hosts <path>` or `--ssh-server-key <path>`
Example:
```sh
cargo run --bin rtr_debug_client -- \
127.0.0.1:22 1 reset \
--ssh \
--ssh-user rpki-rtr \
--ssh-password 'your-password' \
--ssh-server-key certs/ssh_host_rsa_key.pub
```

View File

@ -1001,6 +1001,15 @@ impl Config {
})?; })?;
ensure_ssh_config(&mut transport)?.private_key = Some(PathBuf::from(path)); ensure_ssh_config(&mut transport)?.private_key = Some(PathBuf::from(path));
} }
"--ssh-password" => {
let password = args.next().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"--ssh-password requires a value",
)
})?;
ensure_ssh_config(&mut transport)?.password = Some(password);
}
"--ssh-subsystem" => { "--ssh-subsystem" => {
let subsystem = args.next().ok_or_else(|| { let subsystem = args.next().ok_or_else(|| {
io::Error::new( io::Error::new(
@ -1181,8 +1190,15 @@ impl TransportConfig {
.unwrap_or_else(|| "<none>".to_string()) .unwrap_or_else(|| "<none>".to_string())
), ),
Self::Ssh(cfg) => format!( Self::Ssh(cfg) => format!(
"ssh (user={}, subsystem={}, host_key_check={})", "ssh (user={}, auth={}, subsystem={}, host_key_check={})",
cfg.user.as_deref().unwrap_or("<unset>"), cfg.user.as_deref().unwrap_or("<unset>"),
if cfg.private_key.is_some() {
"publickey"
} else if cfg.password.is_some() {
"password"
} else {
"<unset>"
},
cfg.subsystem cfg.subsystem
.as_deref() .as_deref()
.unwrap_or(DEFAULT_SSH_SUBSYSTEM_NAME), .unwrap_or(DEFAULT_SSH_SUBSYSTEM_NAME),
@ -1222,6 +1238,7 @@ impl HostKeyVerification {
struct SshConfig { struct SshConfig {
user: Option<String>, user: Option<String>,
private_key: Option<PathBuf>, private_key: Option<PathBuf>,
password: Option<String>,
subsystem: Option<String>, subsystem: Option<String>,
known_hosts: Option<PathBuf>, known_hosts: Option<PathBuf>,
server_key: Option<PathBuf>, server_key: Option<PathBuf>,
@ -1315,12 +1332,28 @@ fn finalize_transport(transport: TransportConfig, addr: &str) -> io::Result<Tran
)); ));
} }
let private_key = cfg.private_key.take().ok_or_else(|| { let private_key = cfg.private_key.take();
io::Error::new( let password = cfg.password.take().and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
if private_key.is_some() && password.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"SSH mode requires --ssh-key <path>", "SSH mode authentication must choose one: --ssh-key <path> or --ssh-password <value>",
) ));
})?; }
if private_key.is_none() && password.is_none() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"SSH mode requires authentication: --ssh-key <path> or --ssh-password <value>",
));
}
if cfg.known_hosts.is_some() && cfg.server_key.is_some() { if cfg.known_hosts.is_some() && cfg.server_key.is_some() {
return Err(io::Error::new( return Err(io::Error::new(
@ -1356,7 +1389,8 @@ fn finalize_transport(transport: TransportConfig, addr: &str) -> io::Result<Tran
Ok(TransportConfig::Ssh(SshConfig { Ok(TransportConfig::Ssh(SshConfig {
user: Some(user), user: Some(user),
private_key: Some(private_key), private_key,
password,
subsystem: Some(subsystem), subsystem: Some(subsystem),
known_hosts: None, known_hosts: None,
server_key: None, server_key: None,
@ -1439,10 +1473,24 @@ async fn connect_ssh_stream(addr: &str, ssh: &SshConfig) -> io::Result<DynStream
.user .user
.as_deref() .as_deref()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing SSH user"))?; .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing SSH user"))?;
let private_key_path = ssh let private_key_path = ssh.private_key.as_ref();
.private_key let password = ssh
.as_ref() .password
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing SSH private key"))?; .as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
if private_key_path.is_some() && password.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"SSH auth config invalid: both private key and password provided",
));
}
if private_key_path.is_none() && password.is_none() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"SSH auth config invalid: neither private key nor password provided",
));
}
let subsystem = ssh let subsystem = ssh
.subsystem .subsystem
.as_deref() .as_deref()
@ -1471,38 +1519,53 @@ async fn connect_ssh_stream(addr: &str, ssh: &SshConfig) -> io::Result<DynStream
) )
})?; })?;
let private_key = load_secret_key(private_key_path, None).map_err(|err| { let auth_result = if let Some(private_key_path) = private_key_path {
io::Error::new( let private_key = load_secret_key(private_key_path, None).map_err(|err| {
io::ErrorKind::InvalidInput,
format!(
"failed to load SSH private key {}: {}",
private_key_path.display(),
err
),
)
})?;
let rsa_hash = session.best_supported_rsa_hash().await.map_err(|err| {
io::Error::new(
io::ErrorKind::ConnectionAborted,
format!("failed to negotiate SSH RSA hash: {}", err),
)
})?;
let auth_result = session
.authenticate_publickey(
user.to_string(),
PrivateKeyWithHashAlg::new(Arc::new(private_key), rsa_hash.flatten()),
)
.await
.map_err(|err| {
io::Error::new( io::Error::new(
io::ErrorKind::PermissionDenied, io::ErrorKind::InvalidInput,
format!("SSH publickey authentication failed: {}", err), format!(
"failed to load SSH private key {}: {}",
private_key_path.display(),
err
),
) )
})?; })?;
let rsa_hash = session.best_supported_rsa_hash().await.map_err(|err| {
io::Error::new(
io::ErrorKind::ConnectionAborted,
format!("failed to negotiate SSH RSA hash: {}", err),
)
})?;
session
.authenticate_publickey(
user.to_string(),
PrivateKeyWithHashAlg::new(Arc::new(private_key), rsa_hash.flatten()),
)
.await
.map_err(|err| {
io::Error::new(
io::ErrorKind::PermissionDenied,
format!("SSH publickey authentication failed: {}", err),
)
})?
} else {
session
.authenticate_password(
user.to_string(),
password.expect("password checked above").to_string(),
)
.await
.map_err(|err| {
io::Error::new(
io::ErrorKind::PermissionDenied,
format!("SSH password authentication failed: {}", err),
)
})?
};
if !auth_result.success() { if !auth_result.success() {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::PermissionDenied, io::ErrorKind::PermissionDenied,
"SSH publickey authentication rejected by server", "SSH authentication rejected by server",
)); ));
} }

View File

@ -11,6 +11,7 @@ use tracing::{info, warn};
use rpki::rtr::cache::{RtrCache, SharedRtrCache}; use rpki::rtr::cache::{RtrCache, SharedRtrCache};
use rpki::rtr::payload::Timing; use rpki::rtr::payload::Timing;
use rpki::rtr::server::ssh::SshAuthMode;
use rpki::rtr::server::{RtrNotifier, RtrService, RtrServiceConfig, RunningRtrService}; use rpki::rtr::server::{RtrNotifier, RtrService, RtrServiceConfig, RunningRtrService};
use rpki::rtr::store::RtrStore; use rpki::rtr::store::RtrStore;
use rpki::source::pipeline::{PayloadLoadConfig, load_payloads_from_latest_sources}; use rpki::source::pipeline::{PayloadLoadConfig, load_payloads_from_latest_sources};
@ -33,6 +34,7 @@ struct AppConfig {
ssh_authorized_keys_path: String, ssh_authorized_keys_path: String,
ssh_username: String, ssh_username: String,
ssh_subsystem_name: String, ssh_subsystem_name: String,
ssh_auth_mode: SshAuthMode,
ssh_password: Option<String>, ssh_password: Option<String>,
max_delta: u8, max_delta: u8,
@ -63,6 +65,7 @@ impl Default for AppConfig {
ssh_authorized_keys_path: "./certs/rtr-authorized_keys".to_string(), ssh_authorized_keys_path: "./certs/rtr-authorized_keys".to_string(),
ssh_username: "rpki-rtr".to_string(), ssh_username: "rpki-rtr".to_string(),
ssh_subsystem_name: "rpki-rtr".to_string(), ssh_subsystem_name: "rpki-rtr".to_string(),
ssh_auth_mode: SshAuthMode::Key,
ssh_password: None, ssh_password: None,
max_delta: 100, max_delta: 100,
@ -153,6 +156,14 @@ impl AppConfig {
if let Some(value) = env_var("RPKI_RTR_SSH_SUBSYSTEM_NAME")? { if let Some(value) = env_var("RPKI_RTR_SSH_SUBSYSTEM_NAME")? {
config.ssh_subsystem_name = value; config.ssh_subsystem_name = value;
} }
if let Some(value) = env_var("RPKI_RTR_SSH_AUTH_MODE")? {
config.ssh_auth_mode = SshAuthMode::parse(&value).ok_or_else(|| {
anyhow!(
"invalid RPKI_RTR_SSH_AUTH_MODE '{}': expected key|password|both",
value
)
})?;
}
if let Some(value) = env_var("RPKI_RTR_SSH_PASSWORD")? { if let Some(value) = env_var("RPKI_RTR_SSH_PASSWORD")? {
let value = value.trim().to_string(); let value = value.trim().to_string();
config.ssh_password = if value.is_empty() { None } else { Some(value) }; config.ssh_password = if value.is_empty() { None } else { Some(value) };
@ -368,6 +379,7 @@ fn start_servers(config: &AppConfig, service: &RtrService) -> RunningRtrService
&config.ssh_authorized_keys_path, &config.ssh_authorized_keys_path,
&config.ssh_username, &config.ssh_username,
&config.ssh_subsystem_name, &config.ssh_subsystem_name,
config.ssh_auth_mode,
config.ssh_password.as_deref(), config.ssh_password.as_deref(),
) )
} else if config.enable_tls { } else if config.enable_tls {
@ -388,6 +400,7 @@ fn start_servers(config: &AppConfig, service: &RtrService) -> RunningRtrService
&config.ssh_authorized_keys_path, &config.ssh_authorized_keys_path,
&config.ssh_username, &config.ssh_username,
&config.ssh_subsystem_name, &config.ssh_subsystem_name,
config.ssh_auth_mode,
config.ssh_password.as_deref(), config.ssh_password.as_deref(),
) )
} else { } else {
@ -505,6 +518,7 @@ fn log_startup_config(config: &AppConfig) {
); );
info!("ssh_username={}", config.ssh_username); info!("ssh_username={}", config.ssh_username);
info!("ssh_subsystem_name={}", config.ssh_subsystem_name); info!("ssh_subsystem_name={}", config.ssh_subsystem_name);
info!("ssh_auth_mode={}", config.ssh_auth_mode.as_str());
info!("ssh_password_enabled={}", config.ssh_password.is_some()); info!("ssh_password_enabled={}", config.ssh_password.is_some());
} }

38
src/rtr/cache/core.rs vendored
View File

@ -65,7 +65,7 @@ pub struct VersionState {
impl VersionState { impl VersionState {
fn new(session_id: u16, serial: u32, snapshot: Snapshot, max_delta: u8) -> Self { fn new(session_id: u16, serial: u32, snapshot: Snapshot, max_delta: u8) -> Self {
let rtr_payloads = Arc::new(snapshot.payloads_for_rtr()); let rtr_payloads = snapshot.rtr_payloads_for_rtr_arc();
Self { Self {
session_id, session_id,
serial, serial,
@ -199,7 +199,7 @@ impl RtrCacheBuilder {
VersionState { VersionState {
session_id: session_ids.as_array()[idx], session_id: session_ids.as_array()[idx],
serial: serials[idx], serial: serials[idx],
rtr_payloads: Arc::new(snapshot.payloads_for_rtr()), rtr_payloads: snapshot.rtr_payloads_for_rtr_arc(),
snapshot: Arc::new(snapshot), snapshot: Arc::new(snapshot),
deltas: deltas[idx].clone(), deltas: deltas[idx].clone(),
} }
@ -230,7 +230,7 @@ impl RtrCache {
self.availability = CacheAvailability::NoDataAvailable; self.availability = CacheAvailability::NoDataAvailable;
for version_state in &mut self.versions { for version_state in &mut self.versions {
version_state.snapshot = Arc::new(Snapshot::empty()); version_state.snapshot = Arc::new(Snapshot::empty());
version_state.rtr_payloads = Arc::new(Vec::new()); version_state.rtr_payloads = version_state.snapshot.rtr_payloads_for_rtr_arc();
version_state.deltas.clear(); version_state.deltas.clear();
} }
} }
@ -247,7 +247,7 @@ impl RtrCache {
state.session_id = new_session_ids.get(v); state.session_id = new_session_ids.get(v);
state.serial = 1; state.serial = 1;
state.snapshot = Arc::new(project_snapshot_for_version(source_snapshot, v)); state.snapshot = Arc::new(project_snapshot_for_version(source_snapshot, v));
state.rtr_payloads = Arc::new(state.snapshot.as_ref().payloads_for_rtr()); state.rtr_payloads = state.snapshot.rtr_payloads_for_rtr_arc();
state.deltas.clear(); state.deltas.clear();
} }
self.last_update_end = DualTime::now(); self.last_update_end = DualTime::now();
@ -362,7 +362,7 @@ impl RtrCache {
} }
state.snapshot = Arc::new(projected); state.snapshot = Arc::new(projected);
state.rtr_payloads = Arc::new(state.snapshot.as_ref().payloads_for_rtr()); state.rtr_payloads = state.snapshot.rtr_payloads_for_rtr_arc();
Self::push_delta( Self::push_delta(
state, state,
self.max_delta, self.max_delta,
@ -578,33 +578,7 @@ fn merge_deltas_minimally(current_serial: u32, deltas: &[Arc<Delta>]) -> Delta {
} }
fn project_snapshot_for_version(snapshot: &Snapshot, version: u8) -> Snapshot { fn project_snapshot_for_version(snapshot: &Snapshot, version: u8) -> Snapshot {
let mut payloads = Vec::new(); snapshot.project_for_version(version)
for payload in snapshot.payloads() {
if let Some(projected) = project_payload_for_version(&payload, version) {
payloads.push(projected);
}
}
Snapshot::from_payloads(payloads)
}
fn project_payload_for_version(payload: &Payload, version: u8) -> Option<Payload> {
match payload {
Payload::RouteOrigin(origin) => Some(Payload::RouteOrigin(origin.clone())),
Payload::RouterKey(key) => {
if version >= 1 {
Some(Payload::RouterKey(key.clone()))
} else {
None
}
}
Payload::Aspa(aspa) => {
if version >= 2 {
Some(Payload::Aspa(aspa.clone()))
} else {
None
}
}
}
} }
fn estimate_snapshot_payload_wire_size(snapshot: &Snapshot) -> usize { fn estimate_snapshot_payload_wire_size(snapshot: &Snapshot) -> usize {

157
src/rtr/cache/model.rs vendored
View File

@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::BTreeMap;
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -69,9 +69,9 @@ impl<'de> Deserialize<'de> for DualTime {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot { pub struct Snapshot {
origins: BTreeSet<RouteOrigin>, origins: Arc<Vec<RouteOrigin>>,
router_keys: BTreeSet<RouterKey>, router_keys: Arc<Vec<RouterKey>>,
aspas: BTreeSet<Aspa>, aspas: Arc<Vec<Aspa>>,
created_at: DualTime, created_at: DualTime,
origins_hash: [u8; 32], origins_hash: [u8; 32],
router_keys_hash: [u8; 32], router_keys_hash: [u8; 32],
@ -83,14 +83,26 @@ pub struct Snapshot {
impl Snapshot { impl Snapshot {
pub fn new( pub fn new(
origins: BTreeSet<RouteOrigin>, origins: Vec<RouteOrigin>,
router_keys: BTreeSet<RouterKey>, router_keys: Vec<RouterKey>,
aspas: BTreeSet<Aspa>, aspas: Vec<Aspa>,
) -> Self {
Self::from_shared_parts(
Arc::new(sorted_dedup(origins)),
Arc::new(sorted_dedup(router_keys)),
Arc::new(normalize_aspas(aspas)),
)
}
fn from_shared_parts(
origins: Arc<Vec<RouteOrigin>>,
router_keys: Arc<Vec<RouterKey>>,
aspas: Arc<Vec<Aspa>>,
) -> Self { ) -> Self {
let mut snapshot = Snapshot { let mut snapshot = Snapshot {
origins, origins,
router_keys, router_keys,
aspas: normalize_aspas(aspas), aspas,
created_at: DualTime::now(), created_at: DualTime::now(),
origins_hash: [0u8; 32], origins_hash: [0u8; 32],
router_keys_hash: [0u8; 32], router_keys_hash: [0u8; 32],
@ -105,21 +117,21 @@ impl Snapshot {
} }
pub fn empty() -> Self { pub fn empty() -> Self {
Self::new(BTreeSet::new(), BTreeSet::new(), BTreeSet::new()) Self::new(Vec::new(), Vec::new(), Vec::new())
} }
pub fn from_payloads(payloads: Vec<Payload>) -> Self { pub fn from_payloads(payloads: Vec<Payload>) -> Self {
let mut origins = BTreeSet::new(); let mut origins = Vec::new();
let mut router_keys = BTreeSet::new(); let mut router_keys = Vec::new();
let mut aspas = Vec::new(); let mut aspas = Vec::new();
for p in payloads { for p in payloads {
match p { match p {
Payload::RouteOrigin(o) => { Payload::RouteOrigin(o) => {
origins.insert(o); origins.push(o);
} }
Payload::RouterKey(k) => { Payload::RouterKey(k) => {
router_keys.insert(k); router_keys.push(k);
} }
Payload::Aspa(a) => { Payload::Aspa(a) => {
aspas.push(a); aspas.push(a);
@ -130,6 +142,21 @@ impl Snapshot {
Snapshot::new(origins, router_keys, normalize_aspas(aspas)) Snapshot::new(origins, router_keys, normalize_aspas(aspas))
} }
pub fn project_for_version(&self, version: u8) -> Self {
let router_keys = if version >= 1 {
self.router_keys.clone()
} else {
Arc::new(Vec::new())
};
let aspas = if version >= 2 {
self.aspas.clone()
} else {
Arc::new(Vec::new())
};
Self::from_shared_parts(self.origins.clone(), router_keys, aspas)
}
pub fn recompute_hashes(&mut self) { pub fn recompute_hashes(&mut self) {
self.origins_hash = self.compute_origins_hash(); self.origins_hash = self.compute_origins_hash();
self.router_keys_hash = self.compute_router_keys_hash(); self.router_keys_hash = self.compute_router_keys_hash();
@ -182,21 +209,23 @@ impl Snapshot {
let mut withdrawn = Vec::new(); let mut withdrawn = Vec::new();
if !self.same_origins(new_snapshot) { if !self.same_origins(new_snapshot) {
for origin in new_snapshot.origins.difference(&self.origins) { diff_sorted(
announced.push(Payload::RouteOrigin(origin.clone())); self.origins.as_slice(),
} new_snapshot.origins.as_slice(),
for origin in self.origins.difference(&new_snapshot.origins) { &mut announced,
withdrawn.push(Payload::RouteOrigin(origin.clone())); &mut withdrawn,
} Payload::RouteOrigin,
);
} }
if !self.same_router_keys(new_snapshot) { if !self.same_router_keys(new_snapshot) {
for key in new_snapshot.router_keys.difference(&self.router_keys) { diff_sorted(
announced.push(Payload::RouterKey(key.clone())); self.router_keys.as_slice(),
} new_snapshot.router_keys.as_slice(),
for key in self.router_keys.difference(&new_snapshot.router_keys) { &mut announced,
withdrawn.push(Payload::RouterKey(key.clone())); &mut withdrawn,
} Payload::RouterKey,
);
} }
if !self.same_aspas(new_snapshot) { if !self.same_aspas(new_snapshot) {
@ -268,16 +297,16 @@ impl Snapshot {
self.snapshot_hash == other.snapshot_hash self.snapshot_hash == other.snapshot_hash
} }
pub fn origins(&self) -> &BTreeSet<RouteOrigin> { pub fn origins(&self) -> &[RouteOrigin] {
&self.origins self.origins.as_ref()
} }
pub fn router_keys(&self) -> &BTreeSet<RouterKey> { pub fn router_keys(&self) -> &[RouterKey] {
&self.router_keys self.router_keys.as_ref()
} }
pub fn aspas(&self) -> &BTreeSet<Aspa> { pub fn aspas(&self) -> &[Aspa] {
&self.aspas self.aspas.as_ref()
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
@ -369,15 +398,15 @@ fn build_snapshot_payloads_for_rtr(snapshot: &Snapshot) -> Vec<Payload> {
} }
fn dedup_payloads(payloads: &mut Vec<Payload>) { fn dedup_payloads(payloads: &mut Vec<Payload>) {
let mut seen = BTreeSet::new(); payloads.sort();
payloads.retain(|p| seen.insert(p.clone())); payloads.dedup();
} }
fn normalize_aspas<I>(aspas: I) -> BTreeSet<Aspa> fn normalize_aspas<I>(aspas: I) -> Vec<Aspa>
where where
I: IntoIterator<Item = Aspa>, I: IntoIterator<Item = Aspa>,
{ {
let mut by_customer = BTreeMap::<u32, BTreeSet<_>>::new(); let mut by_customer = BTreeMap::<u32, Vec<_>>::new();
for aspa in aspas { for aspa in aspas {
let providers = by_customer let providers = by_customer
@ -386,17 +415,19 @@ where
providers.extend(aspa.provider_asns().iter().copied()); providers.extend(aspa.provider_asns().iter().copied());
} }
by_customer let mut normalized = by_customer
.into_iter() .into_iter()
.map(|(customer_asn, providers)| { .map(|(customer_asn, providers)| {
Aspa::new(customer_asn.into(), providers.into_iter().collect()) Aspa::new(customer_asn.into(), providers)
}) })
.collect() .collect::<Vec<_>>();
normalized.sort();
normalized
} }
fn diff_aspas( fn diff_aspas(
current: &BTreeSet<Aspa>, current: &[Aspa],
next: &BTreeSet<Aspa>, next: &[Aspa],
announced: &mut Vec<Payload>, announced: &mut Vec<Payload>,
withdrawn: &mut Vec<Payload>, withdrawn: &mut Vec<Payload>,
) { ) {
@ -413,7 +444,7 @@ fn diff_aspas(
.keys() .keys()
.chain(next.keys()) .chain(next.keys())
.copied() .copied()
.collect::<BTreeSet<_>>(); .collect::<std::collections::BTreeSet<_>>();
for customer in customers { for customer in customers {
match (current.get(&customer), next.get(&customer)) { match (current.get(&customer), next.get(&customer)) {
@ -426,3 +457,47 @@ fn diff_aspas(
} }
} }
} }
fn sorted_dedup<T: Ord>(mut items: Vec<T>) -> Vec<T> {
items.sort();
items.dedup();
items
}
fn diff_sorted<T, F>(
current: &[T],
next: &[T],
announced: &mut Vec<Payload>,
withdrawn: &mut Vec<Payload>,
wrap: F,
) where
T: Ord + Clone,
F: Fn(T) -> Payload,
{
let mut i = 0usize;
let mut j = 0usize;
while i < current.len() && j < next.len() {
match current[i].cmp(&next[j]) {
std::cmp::Ordering::Less => {
withdrawn.push(wrap(current[i].clone()));
i += 1;
}
std::cmp::Ordering::Greater => {
announced.push(wrap(next[j].clone()));
j += 1;
}
std::cmp::Ordering::Equal => {
i += 1;
j += 1;
}
}
}
while i < current.len() {
withdrawn.push(wrap(current[i].clone()));
i += 1;
}
while j < next.len() {
announced.push(wrap(next[j].clone()));
j += 1;
}
}

View File

@ -213,14 +213,5 @@ fn persist_update_job(job: StoreSyncJob) {
} }
fn project_snapshot_for_version(snapshot: &Snapshot, version: u8) -> Snapshot { fn project_snapshot_for_version(snapshot: &Snapshot, version: u8) -> Snapshot {
let mut payloads = Vec::new(); snapshot.project_for_version(version)
for payload in snapshot.payloads() {
match payload {
Payload::RouteOrigin(_) => payloads.push(payload),
Payload::RouterKey(_) if version >= 1 => payloads.push(payload),
Payload::Aspa(_) if version >= 2 => payloads.push(payload),
_ => {}
}
}
Snapshot::from_payloads(payloads)
} }

View File

@ -13,7 +13,7 @@ use crate::rtr::cache::SharedRtrCache;
use crate::rtr::server::config::RtrServiceConfig; use crate::rtr::server::config::RtrServiceConfig;
use crate::rtr::server::listener::RtrServer; use crate::rtr::server::listener::RtrServer;
use crate::rtr::server::notifier::RtrNotifier; use crate::rtr::server::notifier::RtrNotifier;
use crate::rtr::server::ssh::load_rtr_ssh_runtime_config; use crate::rtr::server::ssh::{SshAuthMode, load_rtr_ssh_runtime_config};
pub struct RtrService { pub struct RtrService {
cache: SharedRtrCache, cache: SharedRtrCache,
@ -167,6 +167,7 @@ impl RtrService {
authorized_keys_path: impl AsRef<Path>, authorized_keys_path: impl AsRef<Path>,
username: &str, username: &str,
subsystem_name: &str, subsystem_name: &str,
auth_mode: SshAuthMode,
password: Option<&str>, password: Option<&str>,
) -> JoinHandle<()> { ) -> JoinHandle<()> {
let host_key_path = host_key_path.as_ref().to_path_buf(); let host_key_path = host_key_path.as_ref().to_path_buf();
@ -184,6 +185,7 @@ impl RtrService {
&authorized_keys_path, &authorized_keys_path,
&username, &username,
&subsystem_name, &subsystem_name,
auth_mode,
password.as_deref(), password.as_deref(),
inactivity_timeout, inactivity_timeout,
keepalive_interval, keepalive_interval,
@ -213,6 +215,7 @@ impl RtrService {
authorized_keys_path: impl AsRef<Path>, authorized_keys_path: impl AsRef<Path>,
username: &str, username: &str,
subsystem_name: &str, subsystem_name: &str,
auth_mode: SshAuthMode,
password: Option<&str>, password: Option<&str>,
) -> RunningRtrService { ) -> RunningRtrService {
let tcp_handle = self.spawn_tcp(tcp_bind_addr); let tcp_handle = self.spawn_tcp(tcp_bind_addr);
@ -222,6 +225,7 @@ impl RtrService {
authorized_keys_path, authorized_keys_path,
username, username,
subsystem_name, subsystem_name,
auth_mode,
password, password,
); );
@ -244,6 +248,7 @@ impl RtrService {
authorized_keys_path: impl AsRef<Path>, authorized_keys_path: impl AsRef<Path>,
username: &str, username: &str,
subsystem_name: &str, subsystem_name: &str,
auth_mode: SshAuthMode,
password: Option<&str>, password: Option<&str>,
) -> RunningRtrService { ) -> RunningRtrService {
let tcp_handle = self.spawn_tcp(tcp_bind_addr); let tcp_handle = self.spawn_tcp(tcp_bind_addr);
@ -255,6 +260,7 @@ impl RtrService {
authorized_keys_path, authorized_keys_path,
username, username,
subsystem_name, subsystem_name,
auth_mode,
password, password,
); );

View File

@ -9,6 +9,32 @@ use russh::keys::ssh_key::{self, AuthorizedKeys};
use russh::server::Config as RusshServerConfig; use russh::server::Config as RusshServerConfig;
use russh::{MethodKind, MethodSet}; use russh::{MethodKind, MethodSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SshAuthMode {
Key,
Password,
Both,
}
impl SshAuthMode {
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"key" | "publickey" => Some(Self::Key),
"password" => Some(Self::Password),
"both" => Some(Self::Both),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Key => "key",
Self::Password => "password",
Self::Both => "both",
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RtrSshRuntimeConfig { pub struct RtrSshRuntimeConfig {
pub server_config: Arc<RusshServerConfig>, pub server_config: Arc<RusshServerConfig>,
@ -23,6 +49,7 @@ pub fn load_rtr_ssh_runtime_config(
authorized_keys_path: impl AsRef<Path>, authorized_keys_path: impl AsRef<Path>,
username: &str, username: &str,
subsystem_name: &str, subsystem_name: &str,
auth_mode: SshAuthMode,
password: Option<&str>, password: Option<&str>,
inactivity_timeout: Option<Duration>, inactivity_timeout: Option<Duration>,
keepalive_interval: Option<Duration>, keepalive_interval: Option<Duration>,
@ -35,12 +62,21 @@ pub fn load_rtr_ssh_runtime_config(
} }
let host_key = load_host_key(host_key_path.as_ref())?; let host_key = load_host_key(host_key_path.as_ref())?;
let authorized_keys = load_authorized_keys(authorized_keys_path.as_ref())?; let authorized_keys = if matches!(auth_mode, SshAuthMode::Key | SshAuthMode::Both) {
load_authorized_keys(authorized_keys_path.as_ref())?
} else {
Vec::new()
};
let password = password.map(str::trim).filter(|value| !value.is_empty()); let password = password.map(str::trim).filter(|value| !value.is_empty());
if matches!(auth_mode, SshAuthMode::Password | SshAuthMode::Both) && password.is_none() {
bail!("SSH auth mode '{}' requires non-empty password", auth_mode.as_str());
}
let mut methods = MethodSet::empty(); let mut methods = MethodSet::empty();
methods.push(MethodKind::PublicKey); if matches!(auth_mode, SshAuthMode::Key | SshAuthMode::Both) {
if password.is_some() { methods.push(MethodKind::PublicKey);
}
if matches!(auth_mode, SshAuthMode::Password | SshAuthMode::Both) {
methods.push(MethodKind::Password); methods.push(MethodKind::Password);
} }

View File

@ -16,6 +16,7 @@ use tokio_rustls::TlsConnector;
use rpki::rtr::cache::{RtrCacheBuilder, SessionIds, SharedRtrCache}; use rpki::rtr::cache::{RtrCacheBuilder, SessionIds, SharedRtrCache};
use rpki::rtr::payload::Timing; use rpki::rtr::payload::Timing;
use rpki::rtr::pdu::{CacheResponse, EndOfDataV1, ResetQuery}; use rpki::rtr::pdu::{CacheResponse, EndOfDataV1, ResetQuery};
use rpki::rtr::server::ssh::SshAuthMode;
use rpki::rtr::server::RtrService; use rpki::rtr::server::RtrService;
use russh::client; use russh::client;
use russh::keys; use russh::keys;
@ -201,6 +202,7 @@ async fn unified_server_ssh_opens_listener_and_emits_banner() {
&authorized_keys_path, &authorized_keys_path,
"rpki-rtr", "rpki-rtr",
"rpki-rtr", "rpki-rtr",
SshAuthMode::Key,
None, None,
); );
wait_for_port(ssh_addr).await; wait_for_port(ssh_addr).await;
@ -249,6 +251,7 @@ async fn unified_server_ssh_accepts_password_when_configured() {
&authorized_keys_path, &authorized_keys_path,
"rpki-rtr", "rpki-rtr",
"rpki-rtr", "rpki-rtr",
SshAuthMode::Both,
Some("test-password"), Some("test-password"),
); );
wait_for_port(ssh_addr).await; wait_for_port(ssh_addr).await;
@ -323,6 +326,7 @@ async fn unified_server_ssh_rejects_password_when_not_configured() {
&authorized_keys_path, &authorized_keys_path,
"rpki-rtr", "rpki-rtr",
"rpki-rtr", "rpki-rtr",
SshAuthMode::Key,
None, None,
); );
wait_for_port(ssh_addr).await; wait_for_port(ssh_addr).await;