Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-front/src/forward.rs

//! **The forge's public door: tcp/2222 on the front, forwarded to the
//! PRIMARY** (owner 2026-09-25: "keep the old shape" — the front is the public
//! face, `git.gunnar.rs` resolves to it, and `git push` reaches the primary's
//! git listener through it).
//!
//! The target is the verdict, not a setting: the primary's utility address out
//! of what the installer fed ([`target_of`] — `primary_utility`, or the peer
//! whose role is primary), port [`GIT_PORT`]. It is looked at again every
//! [`POLL`], and a change RE-TARGETS LIVE ([`Forward::retarget`]): the next
//! connection goes to the new primary, no restart, and connections already
//! open finish where they began. Until a primary is known a connection is
//! closed at once — refused, never parked.
//!
//! Plain TCP, byte for byte: the SSH session is end to end between the client and the
//! primary, so the host key a client sees IS the primary's. Pure Rust, no nftables, no
//! iptables. The forward itself is edda's ONE copy (`estate_front::forward`, RA5-34): it
//! refuses the front's utility address, holds a client while the primary cannot be dialled
//! yet, caps its connections and lets a silent one go ([`forge_options`]).

use crate::vm::FrontVm;
use estate_front::forward::{Forward, ForwardOptions};
use nordisk_pairing::join::JoinReply;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::Arc;
use std::time::Duration;

/// The forge's port, on the front's every address and on the primary.
pub const GIT_PORT: u16 = 2222;
/// How often the fed reply is looked at again for the primary.
const POLL: Duration = Duration::from_secs(2);
/// How long a git client is held while the primary cannot be dialled yet, dialled again every
/// second. A front that just booted names its primary at once but reaches it only when its
/// tunnel is up: mock gate chain 18 (2026-10-25) measured the relay's first handshake 24 s
/// after the front's boot, and the one 10 s dial before it let the client go (`Connection reset
/// by peer`). An ssh client waits for the banner, so holding it is the kinder answer.
const DIAL_WINDOW: Duration = Duration::from_secs(60);

/// Pure: **where tcp/2222 goes, by the verdict** — the primary's OVERLAY
/// address ([`crate::primary_overlay`]: over the tunnel, WireGuard-only),
/// port [`GIT_PORT`]. `None` until the election has spoken.
pub fn target_of(reply: &JoinReply) -> Option<SocketAddrV4> {
    Some(SocketAddrV4::new(crate::primary_overlay(reply)?, GIT_PORT))
}

/// Pure: **the forge door's options** — `utility` (the front's utility address) refused, a
/// client held [`DIAL_WINDOW`], edda's cap and idle time (RA5-34).
pub fn forge_options(utility: Option<Ipv4Addr>) -> ForwardOptions {
    ForwardOptions {
        refused: utility,
        dial_window: DIAL_WINDOW,
        ..ForwardOptions::default()
    }
}

/// **Run the forward for the life of the process**: listen on every address,
/// port [`GIT_PORT`], and follow the fed verdict. Returns at once.
pub fn start(vm: Arc<FrontVm>) {
    // A32-14, stays a task: binds the forward's listeners for the life of the process (async, ends with the runtime).
    tokio::spawn(async move {
        let utility = appliance_pid1::net::interface_ipv4(crate::UTILITY_INTERFACE)
            .ok()
            .flatten();
        let f = match Forward::bind_with(
            SocketAddr::from((Ipv4Addr::UNSPECIFIED, GIT_PORT)),
            "forge",
            forge_options(utility),
        )
        .await
        {
            Ok(f) => f,
            Err(e) => {
                tracing::error!(error = %e, port = GIT_PORT, "front: the forge forward cannot listen — git.gunnar.rs:2222 will not answer");
                return;
            }
        };
        tracing::info!(listen = %f.local, "front: the forge forward listens; it follows the primary the installer names");
        loop {
            if let Some(reply) = vm.latest() {
                f.retarget(target_of(&reply));
            }
            tokio::time::sleep(POLL).await;
        }
    });
}

#[cfg(test)]
mod tests {
    use super::*;
    use nordisk_pairing::join::{Assigned, Peer};
    use nordisk_pairing::Role;
    use std::collections::BTreeMap;

    fn reply(primary_utility: Option<Ipv4Addr>, peers: Vec<Peer>) -> JoinReply {
        JoinReply {
            estate: "gunnar.rs".into(),
            me: Assigned {
                name: "front".into(),
                overlay: nordisk_pairing::OVERLAY_FRONT,
            },
            cert_pem: None,
            role: Some(Role::Front),
            primary_overlay: None,
            primary_utility,
            peers,
            feed: BTreeMap::new(),
        }
    }

    fn peer(name: &str, role: Role, utility: [u8; 4]) -> Peer {
        // The overlay /32 follows the utility address's last octet (.2 main, .3 twin).
        Peer {
            name: name.into(),
            role,
            tunnel_pubkey: String::new(),
            overlay: Ipv4Addr::new(10, 44, 0, utility[3]),
            endpoint: None,
            utility: Some(Ipv4Addr::from(utility)),
        }
    }

    /// **WireGuard-only** (owner 2026-10-04): the target is the verdict — the
    /// primary's OVERLAY address, never its utility one (refused there), never
    /// the twin's.
    #[test]
    fn the_forward_goes_to_the_primary_the_installer_names_over_the_tunnel() {
        assert_eq!(
            target_of(&reply(None, vec![])),
            None,
            "no verdict, no target"
        );
        let peers = vec![
            peer("twin", Role::Twin, [10, 13, 8, 3]),
            peer("main", Role::Primary, [10, 13, 8, 2]),
        ];
        assert_eq!(
            target_of(&reply(None, peers.clone())),
            Some(SocketAddrV4::new(Ipv4Addr::new(10, 44, 0, 2), 2222))
        );
        let said = JoinReply {
            primary_overlay: Some(Ipv4Addr::new(10, 44, 0, 3)),
            ..reply(Some(Ipv4Addr::new(10, 13, 8, 9)), peers)
        };
        assert_eq!(
            target_of(&said),
            Some(SocketAddrV4::new(Ipv4Addr::new(10, 44, 0, 3), 2222)),
            "the door's primary_overlay wins; primary_utility is never dialled"
        );
    }

    /// ★ **RA5-34: the forge door is edda's one forward**, with the forge's options: the
    /// utility address refused (boxes speak over the tunnel), a client held [`DIAL_WINDOW`]
    /// while the primary cannot be dialled yet (mock gate chain 18), a cap and an idle time.
    #[test]
    fn the_forge_door_is_the_one_forward_with_the_forges_options() {
        let o = forge_options(Some(Ipv4Addr::new(10, 13, 8, 1)));
        assert_eq!(o.refused, Some(Ipv4Addr::new(10, 13, 8, 1)));
        assert_eq!(o.dial_window, DIAL_WINDOW);
        assert!(o.max_connections > 0 && o.idle > Duration::ZERO);
        let code = include_str!("forward.rs");
        let code = &code[..code.find("#[cfg(test)]").unwrap()];
        assert!(
            !code.contains(concat!("copy_", "bidirectional"))
                && !code.contains(concat!("TcpListener", "::bind")),
            "a second forward is written here again"
        );
    }
}