docs: def forward_local(
          self,
          local_port,
          remote_port=None,
          remote_host="localhost",
          local_host="localhost",
      ):
          """
          Open a tunnel connecting ``local_port`` to the server's environment.
  
          For example, say you want to connect to a remote PostgreSQL database
          which is locked down and only accessible via the system it's running
          on. You have SSH access to this server, so you can temporarily make
          port 5432 on your local system act like port 5432 on the server::
  
              import psycopg2
              from fabric import Connection
  
              with Connection('my-db-server').forward_local(5432):
                  db = psycopg2.connect(
                      host='localhost', port=5432, database='mydb'
                  )
                  # Do things with 'db' here
  
          This method is analogous to using the ``-L`` option of OpenSSH's
          ``ssh`` program.
  
          :param int local_port: The local port number on which to listen.
  
          :param int remote_port:
              The remote port number. Defaults to the same value as
              ``local_port``.
  
          :param str local_host:
              The local hostname/interface on which to listen. Default:
              ``localhost``.
  
          :param str remote_host:
              The remote hostname serving the forwarded remote port. Default:
              ``localhost`` (i.e., the host this `.Connection` is connected to.)
  
          :returns:
              Nothing; this method is only useful as a context manager affecting
              local operating system state.
  
          .. versionadded:: 2.0
          """
          if not remote_port:
              remote_port = local_port
  
          # TunnelManager does all of the work, sitting in the background (so we
          # can yield) and spawning threads every time somebody connects to our
          # local port.
          finished = Event()
          manager = TunnelManager(
              local_port=local_port,
              local_host=local_host,
              remote_port=remote_port,
              remote_host=remote_host,
              # TODO: not a huge fan of handing in our transport, but...?
              transport=self.transport,
              finished=finished,
          )
          manager.start()
  
          # Return control to caller now that things ought to be operational
          try:
              yield
          # Teardown once user exits block
          finally:
              # Signal to manager that it should close all open tunnels
              finished.set()
              # Then wait for it to do so
              manager.join()
              # Raise threading errors from within the manager, which would be
              # one of:
              # - an inner ThreadException, which was created by the manager on
              # behalf of its Tunnels; this gets directly raised.
              # - some other exception, which would thus have occurred in the
              # manager itself; we wrap this in a new ThreadException.
              # NOTE: in these cases, some of the metadata tracking in
              # ExceptionHandlingThread/ExceptionWrapper/ThreadException (which
              # is useful when dealing with multiple nearly-identical sibling IO
              # threads) is superfluous, but it doesn't feel worth breaking
              # things up further; we just ignore it for now.
              wrapper = manager.exception()
              if wrapper is not None:
                  if wrapper.type is ThreadException:
                      raise wrapper.value
                  else:
                      raise ThreadException([wrapper])