diff --git a/rust/WEB-SERVER.md b/rust/WEB-SERVER.md index e3a989d76c..b7c186b766 100644 --- a/rust/WEB-SERVER.md +++ b/rust/WEB-SERVER.md @@ -68,8 +68,8 @@ Some more examples: - IPv4, only specific interface: `--address 192.168.1.2:5678` (use the IP address of that interface) -The server can optionally listen on a secondary address, use the `--address2` -option for that. +The server can listen on several addresses, you can use the `--address` option +multiple times. ## Trying the server diff --git a/rust/agama-server/src/agama-web-server.rs b/rust/agama-server/src/agama-web-server.rs index fd149ba0eb..f04f3bacfd 100644 --- a/rust/agama-server/src/agama-web-server.rs +++ b/rust/agama-server/src/agama-web-server.rs @@ -79,13 +79,9 @@ struct Cli { struct ServeArgs { // Address/port to listen on. ":::80" listens for both IPv6 and IPv4 // connections unless manually disabled in /proc/sys/net/ipv6/bindv6only. - /// Primary port to listen on + /// Address:port to listen to, with comma-separated fallback address:port; can be repeated #[arg(long, default_value = ":::80")] - address: String, - - /// Optional secondary address to listen on - #[arg(long, default_value = None)] - address2: Option, + address: Vec, #[arg(long, default_value = "/etc/agama.d/ssl/key.pem")] key: Option, @@ -273,19 +269,28 @@ async fn handle_http_stream( } } +async fn find_listener(addresses: String) -> Option { + let addresses = addresses.split(',').collect::>(); + for addr in addresses { + tracing::info!("Starting Agama web server at {}", addr); + // see https://github.com/tokio-rs/axum/blob/main/examples/low-level-openssl/src/main.rs + // how to use axum with openSSL + match tokio::net::TcpListener::bind(&addr).await { + Ok(listener) => { + return Some(listener); + } + Err(error) => { + tracing::warn!("Error: could not listen on {}: {}", &addr, error); + } + } + } + None +} + /// Starts the web server async fn start_server(address: String, service: Router, ssl_acceptor: SslAcceptor) { - tracing::info!("Starting Agama web server at {}", address); - - // see https://github.com/tokio-rs/axum/blob/main/examples/low-level-openssl/src/main.rs - // how to use axum with openSSL - let listener = tokio::net::TcpListener::bind(&address) - .await - .unwrap_or_else(|error| { - let msg = format!("Error: could not listen on {}: {}", &address, error); - tracing::error!(msg); - panic!("{}", msg) - }); + let opt_listener = find_listener(address).await; + let listener = opt_listener.expect("None of the alternative addresses worked"); pin_mut!(listener); @@ -300,7 +305,7 @@ async fn start_server(address: String, service: Router, ssl_acceptor: SslAccepto let (tcp_stream, addr) = listener .accept() .await - .expect("Failed to open port for listening"); + .expect("Failed to accept connection"); tokio::spawn(async move { if is_ssl_stream(&tcp_stream).await { @@ -341,13 +346,8 @@ async fn serve_command(args: ServeArgs) -> anyhow::Result<()> { return Err(anyhow::anyhow!("SSL initialization failed")); }; - let mut addresses = vec![args.address]; - - if let Some(a) = args.address2 { - addresses.push(a) - } - - let servers: Vec<_> = addresses + let servers: Vec<_> = args + .address .iter() .map(|a| { tokio::spawn(start_server( diff --git a/rust/install.sh b/rust/install.sh index b59b975ce7..9df916d5f9 100755 --- a/rust/install.sh +++ b/rust/install.sh @@ -38,6 +38,7 @@ install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/target/${RUST_TARGET}/agama" install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/target/${RUST_TARGET}/agama-autoinstall" install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/target/${RUST_TARGET}/agama-proxy-setup" install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/target/${RUST_TARGET}/agama-web-server" +install -D -t "${DESTDIR}${bindir}" "${SRCDIR}/share/agama-web-server.sh" install6 -D -p "${SRCDIR}"/share/agama.pam "${DESTDIR}${pamvendordir}"/agama diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 3e0f33f38c..6e47d5f222 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,13 @@ +------------------------------------------------------------------- +Thu Mar 12 13:05:51 UTC 2026 - Ladislav Slezák + +- Added support for the "inst.listen_on" boot parameter + - The "inst.listen_on=localhost" option disables remote access + - It is possible to specify IP address (both IPv4 and IPv6) + or network interface, accepts comma separated list + - agama-web-server --address2 option removed, --address can now be repeated + (jsc#AGM-153) + ------------------------------------------------------------------- Thu Mar 12 10:03:39 UTC 2026 - Imobach Gonzalez Sosa diff --git a/rust/package/agama.spec b/rust/package/agama.spec index 0d284c1484..8de4f91cf5 100644 --- a/rust/package/agama.spec +++ b/rust/package/agama.spec @@ -245,6 +245,7 @@ echo $PATH %doc README.md %license LICENSE %{_bindir}/agama-web-server +%{_bindir}/agama-web-server.sh %{_bindir}/agama-proxy-setup %{_pam_vendordir}/agama %{_unitdir}/agama-web-server.service diff --git a/rust/share/agama-web-server.service b/rust/share/agama-web-server.service index b17151987c..3f96c86326 100644 --- a/rust/share/agama-web-server.service +++ b/rust/share/agama-web-server.service @@ -9,7 +9,7 @@ BindsTo=agama.service EnvironmentFile=-/run/agama/environment.conf Environment="AGAMA_LOG=debug,zbus=info" Type=notify -ExecStart=/usr/bin/agama-web-server serve --address :::80 --address2 :::443 +ExecStart=/usr/bin/agama-web-server.sh PIDFile=/run/agama/web.pid User=root TimeoutStopSec=5 diff --git a/rust/share/agama-web-server.sh b/rust/share/agama-web-server.sh new file mode 100755 index 0000000000..55291fdf94 --- /dev/null +++ b/rust/share/agama-web-server.sh @@ -0,0 +1,97 @@ +#!/usr/bin/bash +# +# Copyright (c) [2026] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +# This script is a wrapper for the Agama web server, it evaluates to which +# addresses the server should listen to. + +if [[ "$1" == "-h" || "$1" == "--help" ]]; then + echo "Usage: $0" + echo + echo " This is a wrapper script for the Agama web server (agama-web-server)." + echo + echo " It configures the listening addresses for the web server based on" + echo " the \"inst.listen_on\" boot option." + exit 0 +fi + +# the default options: listen on all interfaces for both HTTP and HTTPS ports, +# the IPv4 addresses are fallbacks when IPv6 is disabled with the +# "ipv6.disable=1" kernel boot option +DEFAULT_OPTIONS=(--address ":::80,0.0.0.0:80" --address ":::443,0.0.0.0:443") +# options for localhost access only +LOCAL_OPTIONS=(--address "::1:80,127.0.0.1:80" --address "::1:443,127.0.0.1:443") + +# check if the "inst.listen_on=" boot option was used +if grep -q "\binst.listen_on=.\+" /run/agama/cmdline.d/agama.conf; then + LISTEN_ON=$(grep "\binst.listen_on=.\+" /run/agama/cmdline.d/agama.conf | sed 's/.*\binst.listen_on=\([^[:space:]]\+\)/\1/') + + if [ "$LISTEN_ON" = "localhost" ]; then + OPTIONS=("${LOCAL_OPTIONS[@]}") + elif [ "$LISTEN_ON" = "all" ]; then + OPTIONS=("${DEFAULT_OPTIONS[@]}") + else + # always run on the localhost + OPTIONS=("${LOCAL_OPTIONS[@]}") + + # the string can contain multiple addresses separated by comma, + # replace commas with spaces and iterate over items + ADDRESSES=${LISTEN_ON//,/ } + for ADDRESS in $ADDRESSES; do + # check if the value is an IP address (IPv6, IPv6 link local or IPv4) + if echo "$ADDRESS" | grep -qE '^[0-9a-fA-F:]+$|^[fF][eE]80|^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then + echo "<5>Listening on IP address ${ADDRESS}" + OPTIONS+=(--address "${ADDRESS}:80" --address "${ADDRESS}:443") + else + # otherwise assume it is as an interface name + if ip addr show dev "${ADDRESS}" >/dev/null 2>&1; then + # find the IP address for the specified interface + IP_ADDRS=$(ip -o addr show dev "${ADDRESS}" | awk '{print $4}' | cut -d/ -f1) + if [ -n "${IP_ADDRS}" ]; then + for IP in $IP_ADDRS; do + # append the %device for link local IPv6 addresses + if [[ "$IP" == fe80* ]]; then + IP="${IP}%${ADDRESS}" + fi + echo "<5>Listening on interface ${ADDRESS} with IP address ${IP}" + OPTIONS+=(--address "${IP}:80" --address "${IP}:443") + done + else + echo "<3>IP address for interface ${ADDRESS} not found" + fi + else + echo "<3>Network Interface ${ADDRESS} not found" + fi + fi + done + fi +else + OPTIONS=("${DEFAULT_OPTIONS[@]}") +fi + +if [ "${OPTIONS[*]}" = "${DEFAULT_OPTIONS[*]}" ]; then + echo "<5>Listening on all network interfaces" +elif [ "${OPTIONS[*]}" = "${LOCAL_OPTIONS[*]}" ]; then + echo "<5>Disabling remote access to the Agama web server" +fi + +echo "<5>Starting Agama web server with options: ${OPTIONS[*]}" +exec /usr/bin/agama-web-server serve "${OPTIONS[@]}" diff --git a/rust/zypp-agama/src/lib.rs b/rust/zypp-agama/src/lib.rs index b4416a6aba..509a040d65 100644 --- a/rust/zypp-agama/src/lib.rs +++ b/rust/zypp-agama/src/lib.rs @@ -597,8 +597,7 @@ impl Zypp { // save the solver testcase if the solver run failed or if saving is forced via boot // parameter, skip when "ZYPP_FULLLOG=1", in that case libzypp creates the solver // testcase automatically in the /var/log/YaST2/autoTestcase/ directory - if (!r_res || save_testcase) && std::env::var("ZYPP_FULLLOG").unwrap_or_default() != "1" - { + if (!r_res || save_testcase) && !std::env::var("ZYPP_FULLLOG").is_ok_and(|v| v == "1") { self.create_solver_testcase(); } else { // delete the solver testcase directory, it contains the previous error which is diff --git a/rust/zypp-agama/zypp-agama-sys/c-layer/include/lib.h b/rust/zypp-agama/zypp-agama-sys/c-layer/include/lib.h index 9d8f3d23a5..8059d919b6 100644 --- a/rust/zypp-agama/zypp-agama-sys/c-layer/include/lib.h +++ b/rust/zypp-agama/zypp-agama-sys/c-layer/include/lib.h @@ -231,7 +231,7 @@ bool is_package_selected(struct Zypp *zypp, const char *tag, bool run_solver(struct Zypp *zypp, bool only_required, struct Status *status) noexcept; -/// Create a solver testcase, dumps all all solver data (repositories, loaded +/// Create a solver testcase, dumps all solver data (repositories, loaded /// packages...) to disk /// @param zypp see \ref init_target /// @param dir directory path where the solver testcase is saved diff --git a/rust/zypp-agama/zypp-agama-sys/src/bindings.rs b/rust/zypp-agama/zypp-agama-sys/src/bindings.rs index 1adcaaaa1c..4aa288bc60 100644 --- a/rust/zypp-agama/zypp-agama-sys/src/bindings.rs +++ b/rust/zypp-agama/zypp-agama-sys/src/bindings.rs @@ -674,7 +674,7 @@ unsafe extern "C" { ) -> bool; #[doc = " Runs solver\n @param zypp see \\ref init_target\n @param only_required if true, only required packages are installed (ignoring\n recommended packages)\n @param[out] status (will overwrite existing contents)\n @return true if solver pass and false if it found some dependency issues"] pub fn run_solver(zypp: *mut Zypp, only_required: bool, status: *mut Status) -> bool; - #[doc = " Create a solver testcase, dumps all all solver data (repositories, loaded\n packages...) to disk\n @param zypp see \\ref init_target\n @param dir directory path where the solver testcase is saved\n @return true if the solver testcase was successfully created"] + #[doc = " Create a solver testcase, dumps all solver data (repositories, loaded\n packages...) to disk\n @param zypp see \\ref init_target\n @param dir directory path where the solver testcase is saved\n @return true if the solver testcase was successfully created"] pub fn create_solver_testcase(zypp: *mut Zypp, dir: *const ::std::os::raw::c_char) -> bool; #[doc = " the last call that will free all pointers to zypp holded by agama"] pub fn free_zypp(zypp: *mut Zypp); diff --git a/web/README.md b/web/README.md index aeb005e3ab..91d327f532 100644 --- a/web/README.md +++ b/web/README.md @@ -41,7 +41,7 @@ Example of running from different machine: # backend machine # using ip of machine instead of localhost is important to be network accessible # second address is needed for SSL which is mandatory for remote access - agama-web-server serve --address :::80 --address2 :::443 + agama-web-server serve --address :::80 --address :::443 # frontend machine # ESLINT=0 is useful to ignore linter problems during development