Ferreteria/v0.6/clade/IO/Aspect/Connx/aux/Tunnel: Difference between revisions

From Woozle Writes Code
< Ferreteria‎ | v0.6‎ | clade‎ | IO‎ | Aspect‎ | Connx‎ | aux
Jump to navigation Jump to search
m (Woozle moved page Ferreteria/v0.6/clade/IO/Aspect/Creds/aux/Tunnel to Ferreteria/v0.6/clade/IO/Aspect/Connx/aux/Tunnel without leaving a redirect: wrong namespace)
No edit summary
 
Line 11: Line 11:
{{!-!}} '''CredsIface'''  {{!!}} <code>{{l/ver/clade/full|IO\Aspect|Creds}}</code>
{{!-!}} '''CredsIface'''  {{!!}} <code>{{l/ver/clade/full|IO\Aspect|Creds}}</code>
{{!-!}} '''Action''' [c,i] {{!!}} <code>{{l/ver/clade/full|Sys\Events|ItWent}}</code>
{{!-!}} '''Action''' [c,i] {{!!}} <code>{{l/ver/clade/full|Sys\Events|ItWent}}</code>
}}
==About==
===Usage===
There is no need to open a Shell connection before creating a Tunnel.
===Background===
As of PHP v8.3.6 (2024-09-30), the SSH tunneling function ([https://www.php.net/manual/en/function.ssh2-tunnel.php <code>ssh2_tunnel()</code>]) assumes that the port number to forward locally and the port number to connect to on the remote are the same. This is a problem if you are trying to connect to a remote service which is also running locally (most often a database engine).
([https://blog.rjmetrics.com/2009/01/06/php-mysql-and-ssh-tunneling-port-forwarding/ Apparently] this has been the case since 2009.)
In order to make practical use of tunnels, then, it is necessary to use other means -- and what seems to work is to create the tunnel via a shell command.
This class is a wrapper around that functionality. If <code>ssh2_tunnel()</code> is ever fixed to allow different ports, then the CLI functionality can be replaced by calls to that function.
====Points to Remember====
* There are in fact ''three'' different port numbers involved:
** local port to be forwarded
** remote port for ssh connection (usually 22)
** remote service port (to which we want to forward the local port)
* Although we use 2 Credentials objects (outer for ssh and inner for the remote service), we don't use the "inside user" here. That's used by our local client when actually connecting through the port. Here's what we do use here:
** '''local port'''
** '''outside creds''': host, port, user
** '''inside creds''': host, port
* There is apparently no ''good'' way of finding an unallocated port in Linux, though there are ways. For now I'm kluging some defaults.
==History==
* '''{{fmt/date|2024|11|26}}''' moved from [WF]<code>IO\Connx</code> &rArr; [WF]<code>IO\Connx\Aux\ssh</code> in order to better integrate with ssh\xShell
* '''{{fmt/date|2025|06|03}}''' Temporarily retired from [WFe]<code>IO\Connx\Aux\ssh</code>, then rescued to [WFe]<code>IO\Aspect\Connx\aux</code>
==Code==
===current===
''as of {{fmt/date|2025|12|08}}:''
{{fmt/php/block|1=
interface iTunnel extends BaseIface {
    // ACTION
    function Open() : ActionIface;
    function Shut() : ActionIface;
    // SETTINGS
    function QLogSpec() : QStrIface;
}
class cTunnel extends BaseClass implements iTunnel {
    // ++ CONFIG ++ //
    protected function DefaultLogFileName() : string { return 'ssh-tunnel.log'; }
    protected function DefaultShellPort() : int { return 22; }    // this should probably refer to Secure somehow
    protected function DefaultTunnelPort() : int { return 3307; } // this is kind of arbitrary
    // -- CONFIG -- //
    // ++ SETUP ++ //
    public function __construct(private CredsIface $oOCreds, private CredsIface $oICreds, private ?int $nPortLocal=NULL) {}
    protected function OuterCreds() : CredsIface { return $this->oOCreds; }
    protected function InnerCreds() : CredsIface { return $this->oICreds; }
    protected function LocalPortGiven() : ?int { return $this->nPortLocal; }
    // -- SETUP -- //
    // ++ ACTION ++ //
    /**
    * HISTORY:
    *  2024-11-24 started writing as OpenTunnel() in Shell class
    *  2024-11-25 made Tunnel into separate class/iface
    *    Now using shell ommand instead of ssh2_tunnel() (see docs:Background).
    *  2025-06-02 This will need updating.
    */
    public function Open() : ActionIface {
        $oAct = new ActionClass;
        $oOCreds = $this->OuterCreds();
        $oICreds = $this->InnerCreds();
        // OUTSIDE: creds for outside tunnel connection, i.e. how to connect the tunnel to the remote server
        // - local port to forward through tunnel
        $osOPort = $oOCreds->QPort(); // is one explicitly set?
        if ($osOPort->HasIt()) {
            $nOPort = $osOPort->GetIt();
            $sOPort = " -p $nOPort"; // command syntax to explicitly set ssh port
        } else {
            $sOPort = ''; // ssh should just use its configured default
        }
        $sOHost = $oOCreds->QHost()->GetIt(); // local-relative host for tunnel connection
        $sOUser = $oOCreds->QUser()->GetIt(); // user for local to invoke on remote host, for tunnel gateway
        // INSIDE creds for *inside* the tunnel connection, i.e. from the remote end of the tunnel
        $nIPort = $oICreds->QPort()->GetItNz($this->DefaultShellPort()); // remote service port - final destination port on remote
        $sIHost = $oICreds->QHost()->GetIt(); // service host - for remote end of tunnel to connect to ("inside host")
        $nLPort = $this->LPort(); // local port to be forwarded
        $fsLog = $this->QLogSpec()->GetIt();
        $sCmd = "ssh$sOPort -f -L $nLPort:$sOHost:$nIPort $sOUser@$sOHost sleep 60 >> $fsLog";  // TODO: "60" should be configurable
        $oScrn = self::Screen();
        echo $oScrn->InfoIt('COMMAND').': '.$oScrn->GreenIt($sCmd).CRLF;
        $ok = shell_exec($sCmd);
        if (is_null($ok)) {
            $oAct->SetOkay(TRUE);
            $oAct->AddMsgString('There was no output (might be ok?): '.$sCmd);
        } elseif ($ok === FALSE) {
            $oAct->SetOkay(FALSE);
            $oAct->AddMsgString('Pipe could not be established for: '.$sCmd);
        } else {
            $oAct->SetOkay(TRUE);
            $oAct->AddMsgString($ok);
        }
        // The above will probably want some refinement. Maybe a podling-class that separately tracks the output and command.
        // We also might want to use exec() instead.
        return $oAct;
    }
    public function Shut() : ActionIface {
        $oAct = new ActionClass;
        $oAct->SetNoOp(); // tunnel times out when abandoned; not sure if there's a way to close it explicitly
        return $oAct;
    }
    // -- ACTION -- //
    // ++ SETTINGS ++ //
/*
    // Remote Host (for tunnel connection)
    protected function RHost() : string  { return $this->TCreds()->QHost()->GetIt(); }
    // Remote User (for tunnel connection)
    protected function RUser() : string  { return $this->TCreds()->QUser()->GetIt(); }
    // Internal Host (connecting from tunnel)
    protected function IHost() : string  { return $this->TCreds()->QHost()->GetIt(); }
*/
    // Inner Port (connecting from tunnel)
    protected function IPort() : int    { return $this->InnerCreds()->QPort()->GetIt(); }
    /**
    * RETURNS: Local Port (to be forwarded to IPort through tunnel)
    * DETAILS:
    *  * If this was specified at setup, then use that.
    *  * If not, then use LPortDefaul().
    *  * Either way, set QPort() so clients trying to use the tunnel will know what port to use.
    */
    protected function LPort() : int    {
        $nPort = $this->LocalPortGiven();
        if (is_null($nPort)) {
            $nPort = $this->LPortDefault();
        }
        $this->QPort()->SetIt($nPort);
        return $nPort;
    }
    // This is set to whatever actually ends up getting used when the tunnel was opened:
    private $osPort = NULL;
    public function QPort() : QIntIface { return $this->osPort ?? ($this->osPort = QIntClass::AsNew()); }
    /**
    * RETURNS: Local (Outer) Port Default (for when it's not specified at setup)
    * DETAILS:
    *  * If the ICreds have a port set, then take the next port after that (because [waves hands], e.g. "it works for 3306").
    *  * If they don't, then use the configured default. (Maybe we should just always do this)
    */
    protected function LPortDefault() : int {
        $osIPort = $this->InnerCreds()->QPort();
        if ($osIPort->HasIt()) {
            $nPort = $osIPort->GetIt()+1;
        } else {
            $nPort = $this->DefaultTunnelPort();
        }
        return $nPort;
    }
    // externally-settable name for logging command output
    private $osLogSpec = NULL;
    public function QLogSpec()        : QStrIface { return $this->osLogSpec ?? ($this->osLogSpec = $this->NewQLogSpec()); }
    protected function NewQLogSpec()  : QStrIface { return QStrClass::FromString($this->DefaultLogFileName()); }
    // -- SETTINGS -- //
}
}}
===removed===
===={{fmt/date|2024|11|25}}====
This could have worked if not for the different-ports issue (but it would be necessary to open the SSH connection *first*):
{{fmt/php/block|1=#
    public function Open() : ActionIface {
        $oAct = new ActionClass;
        $osShNat = $this->Shell()->QNative();
        if ($osShNat->HasIt()) {
            $rShell = $osShNat->GetIt();
            // Create SSH tunnel
            $rTunn = ssh2_tunnel($rShell, 'localhost', $nPort);
            // Function has no known error conditions.
            $this->QNative()->SetIt($rTunn);
            $oAct->SetOkay(TRUE);
        } else {
            $oAct->SetOkay(FALSE);
            $oAct->SetMessage("Connection isn't open, so cannot open tunnel.");
        }
        return $oAct;
    }
}}
}}

Latest revision as of 16:43, 8 December 2025

clade: IO\Aspect\Connx\aux\Tunnel
Clade Family
StandardBase Tunnel (none)
Clade Aliases
Alias Clade
Base* [c,i] Aux\StandardBase
QInt* [c,i] Data\Mem\QVar\Int
QStr* [c,i] Data\Mem\QVar\Str
CredsIface IO\Aspect\Creds
Action [c,i] Sys\Events\ItWent
Subpages

About

Usage

There is no need to open a Shell connection before creating a Tunnel.

Background

As of PHP v8.3.6 (2024-09-30), the SSH tunneling function (ssh2_tunnel()) assumes that the port number to forward locally and the port number to connect to on the remote are the same. This is a problem if you are trying to connect to a remote service which is also running locally (most often a database engine).

(Apparently this has been the case since 2009.)

In order to make practical use of tunnels, then, it is necessary to use other means -- and what seems to work is to create the tunnel via a shell command.

This class is a wrapper around that functionality. If ssh2_tunnel() is ever fixed to allow different ports, then the CLI functionality can be replaced by calls to that function.

Points to Remember

  • There are in fact three different port numbers involved:
    • local port to be forwarded
    • remote port for ssh connection (usually 22)
    • remote service port (to which we want to forward the local port)
  • Although we use 2 Credentials objects (outer for ssh and inner for the remote service), we don't use the "inside user" here. That's used by our local client when actually connecting through the port. Here's what we do use here:
    • local port
    • outside creds: host, port, user
    • inside creds: host, port
  • There is apparently no good way of finding an unallocated port in Linux, though there are ways. For now I'm kluging some defaults.

History

  • 2024-11-26 moved from [WF]IO\Connx ⇒ [WF]IO\Connx\Aux\ssh in order to better integrate with ssh\xShell
  • 2025-06-03 Temporarily retired from [WFe]IO\Connx\Aux\ssh, then rescued to [WFe]IO\Aspect\Connx\aux

Code

current

as of 2025-12-08:

interface iTunnel extends BaseIface {
    // ACTION
    function Open() : ActionIface;
    function Shut() : ActionIface;
    // SETTINGS
    function QLogSpec() : QStrIface;
}
class cTunnel extends BaseClass implements iTunnel {

    // ++ CONFIG ++ //

    protected function DefaultLogFileName() : string { return 'ssh-tunnel.log'; }
    protected function DefaultShellPort() : int { return 22; }    // this should probably refer to Secure somehow
    protected function DefaultTunnelPort() : int { return 3307; } // this is kind of arbitrary

    // -- CONFIG -- //
    // ++ SETUP ++ //

    public function __construct(private CredsIface $oOCreds, private CredsIface $oICreds, private ?int $nPortLocal=NULL) {}
    protected function OuterCreds() : CredsIface { return $this->oOCreds; }
    protected function InnerCreds() : CredsIface { return $this->oICreds; }
    protected function LocalPortGiven() : ?int { return $this->nPortLocal; }

    // -- SETUP -- //
    // ++ ACTION ++ //

    /**
     * HISTORY:
     *  2024-11-24 started writing as OpenTunnel() in Shell class
     *  2024-11-25 made Tunnel into separate class/iface
     *    Now using shell ommand instead of ssh2_tunnel() (see docs:Background).
     *  2025-06-02 This will need updating.
     */
    public function Open() : ActionIface {
        $oAct = new ActionClass;

        $oOCreds = $this->OuterCreds();
        $oICreds = $this->InnerCreds();

        // OUTSIDE: creds for outside tunnel connection, i.e. how to connect the tunnel to the remote server

        // - local port to forward through tunnel
        $osOPort = $oOCreds->QPort(); // is one explicitly set?
        if ($osOPort->HasIt()) {
            $nOPort = $osOPort->GetIt();
            $sOPort = " -p $nOPort"; // command syntax to explicitly set ssh port
        } else {
            $sOPort = ''; // ssh should just use its configured default
        }
        $sOHost = $oOCreds->QHost()->GetIt(); // local-relative host for tunnel connection
        $sOUser = $oOCreds->QUser()->GetIt(); // user for local to invoke on remote host, for tunnel gateway

        // INSIDE creds for *inside* the tunnel connection, i.e. from the remote end of the tunnel
        $nIPort = $oICreds->QPort()->GetItNz($this->DefaultShellPort()); // remote service port - final destination port on remote
        $sIHost = $oICreds->QHost()->GetIt(); // service host - for remote end of tunnel to connect to ("inside host")

        $nLPort = $this->LPort(); // local port to be forwarded

        $fsLog = $this->QLogSpec()->GetIt();

        $sCmd = "ssh$sOPort -f -L $nLPort:$sOHost:$nIPort $sOUser@$sOHost sleep 60 >> $fsLog";  // TODO: "60" should be configurable
        $oScrn = self::Screen();
        echo $oScrn->InfoIt('COMMAND').': '.$oScrn->GreenIt($sCmd).CRLF;

        $ok = shell_exec($sCmd);
        if (is_null($ok)) {
            $oAct->SetOkay(TRUE);
            $oAct->AddMsgString('There was no output (might be ok?): '.$sCmd);
        } elseif ($ok === FALSE) {
            $oAct->SetOkay(FALSE);
            $oAct->AddMsgString('Pipe could not be established for: '.$sCmd);
        } else {
            $oAct->SetOkay(TRUE);
            $oAct->AddMsgString($ok);
        }
        // The above will probably want some refinement. Maybe a podling-class that separately tracks the output and command.
        // We also might want to use exec() instead.

        return $oAct;
    }
    public function Shut() : ActionIface {
        $oAct = new ActionClass;
        $oAct->SetNoOp(); // tunnel times out when abandoned; not sure if there's a way to close it explicitly
        return $oAct;
    }

    // -- ACTION -- //
    // ++ SETTINGS ++ //
/*
    // Remote Host (for tunnel connection)
    protected function RHost() : string  { return $this->TCreds()->QHost()->GetIt(); }

    // Remote User (for tunnel connection)
    protected function RUser() : string  { return $this->TCreds()->QUser()->GetIt(); }

    // Internal Host (connecting from tunnel)
    protected function IHost() : string  { return $this->TCreds()->QHost()->GetIt(); }
*/
    // Inner Port (connecting from tunnel)
    protected function IPort() : int     { return $this->InnerCreds()->QPort()->GetIt(); }

    /**
     * RETURNS: Local Port (to be forwarded to IPort through tunnel)
     * DETAILS:
     *  * If this was specified at setup, then use that.
     *  * If not, then use LPortDefaul().
     *  * Either way, set QPort() so clients trying to use the tunnel will know what port to use.
    */
    protected function LPort() : int     {
        $nPort = $this->LocalPortGiven();
        if (is_null($nPort)) {
            $nPort = $this->LPortDefault();
        }
        $this->QPort()->SetIt($nPort);
        return $nPort;
    }

    // This is set to whatever actually ends up getting used when the tunnel was opened:
    private $osPort = NULL;
    public function QPort() : QIntIface { return $this->osPort ?? ($this->osPort = QIntClass::AsNew()); }

    /**
     * RETURNS: Local (Outer) Port Default (for when it's not specified at setup)
     * DETAILS:
     *  * If the ICreds have a port set, then take the next port after that (because [waves hands], e.g. "it works for 3306").
     *  * If they don't, then use the configured default. (Maybe we should just always do this)
     */
    protected function LPortDefault() : int {
        $osIPort = $this->InnerCreds()->QPort();
        if ($osIPort->HasIt()) {
            $nPort = $osIPort->GetIt()+1;
        } else {
            $nPort = $this->DefaultTunnelPort();
        }
        return $nPort;
    }

    // externally-settable name for logging command output
    private $osLogSpec = NULL;
    public function QLogSpec()        : QStrIface { return $this->osLogSpec ?? ($this->osLogSpec = $this->NewQLogSpec()); }
    protected function NewQLogSpec()  : QStrIface { return QStrClass::FromString($this->DefaultLogFileName()); }

    // -- SETTINGS -- //
}

removed

2024-11-25

This could have worked if not for the different-ports issue (but it would be necessary to open the SSH connection *first*):

#
    public function Open() : ActionIface {
        $oAct = new ActionClass;
        $osShNat = $this->Shell()->QNative();
        if ($osShNat->HasIt()) {
            $rShell = $osShNat->GetIt();
            // Create SSH tunnel
            $rTunn = ssh2_tunnel($rShell, 'localhost', $nPort);
            // Function has no known error conditions.
            $this->QNative()->SetIt($rTunn);
            $oAct->SetOkay(TRUE);
        } else {
            $oAct->SetOkay(FALSE);
            $oAct->SetMessage("Connection isn't open, so cannot open tunnel.");
        }
        return $oAct;
    }