xpra icon
Bug tracker and wiki

Ticket #2041: p3.txt

File p3.txt, 36.5 KB (added by Nathan Hallquist, 6 months ago)
Line 
1Index: man/xpra.1
2===================================================================
3--- man/xpra.1  (revision 21176)
4+++ man/xpra.1  (working copy)
5@@ -88,7 +88,7 @@
6 Connect using websocket protocol.
7 .SS wss://[USERNAME[:PASSWORD]@]HOST:PORT/[DISPLAY]
8 Connect using secure websocket protocol. (websocket with SSL)
9-.SS ssh://[USERNAME[:PASSWORD]@]HOST[:SSH_PORT]/DISPLAY
10+.SS ssh://[USERNAME[:PASSWORD]@]HOST[:SSH_PORT]/DISPLAY[?proxy=ssh://[USERNAME[:PASSWORD]@]HOST[:SSH_PORT]]
11 Further options can be specified using the \fB\-\-ssh\fP command line option.
12 .P
13 For backwards compatibility, SSH mode also supports the syntax:
14@@ -101,6 +101,10 @@
15 The password need only be specified when the server authentication module
16 requires it. (ie: often when authenticating against MS Windows servers,
17 or with \fBmultifile\fP and \fBsqlite\fP authentication modules)
18+.P
19+When the "?proxy=" option is set XPRA will establish an SSH connection to the
20+specified "proxy" host.  From that host XPRA will set up an SSH connection to
21+the XPRA server.
22 .\" --------------------------------------------------------------------
23 .SH EXAMPLES
24 .TP \w'xpra\ 'u
25Index: xpra/client/gtk_base/client_launcher.py
26===================================================================
27--- xpra/client/gtk_base/client_launcher.py     (revision 21176)
28+++ xpra/client/gtk_base/client_launcher.py     (working copy)
29@@ -40,7 +40,8 @@
30 from xpra.client.gtk_base.gtk_tray_menu_base import make_min_auto_menu, make_encodingsmenu, \
31                                     MIN_QUALITY_OPTIONS, QUALITY_OPTIONS, MIN_SPEED_OPTIONS, SPEED_OPTIONS
32 from xpra.gtk_common.about import about
33-from xpra.scripts.main import connect_to, make_client, configure_network, is_local, add_ssh_args, parse_ssh_string
34+from xpra.scripts.main import connect_to, make_client, configure_network, is_local, add_ssh_args, parse_ssh_string, \
35+                                    add_ssh_proxy_args
36 from xpra.platform.paths import get_icon_dir
37 from xpra.platform import get_username
38 from xpra.log import Logger, enable_debug_for
39@@ -48,7 +49,9 @@
40 
41 #what we save in the config file:
42 SAVED_FIELDS = ["username", "password", "host", "port", "mode", "ssh_port",
43-                "encoding", "quality", "min-quality", "speed", "min-speed"]
44+                "encoding", "quality", "min-quality", "speed", "min-speed",
45+                "proxy_ssh_port", "proxy_username", "proxy_pkey_path",
46+                "proxy_password" ]
47 
48 #options not normally found in xpra config file
49 #but which can be present in a launcher config:
50@@ -56,19 +59,29 @@
51                         "host"              : str,
52                         "port"              : int,
53                         "username"          : str,
54+                        "proxy_username"    : str,
55+                        "proxy_host"        : str,
56+                        "proxy_password"    : str,
57+                        "proxy_pkey_path"   : str,
58                         "password"          : str,
59                         "mode"              : str,
60                         "autoconnect"       : bool,
61                         "ssh_port"          : int,
62+                        "proxy_ssh_port"    : int,
63                         }
64 LAUNCHER_DEFAULTS = {
65                         "host"              : "",
66                         "port"              : -1,
67                         "username"          : get_username(),
68+                        "proxy_username"    : get_username(),
69+                        "proxy_host"        : "",
70+                        "proxy_pkey_path"   : "",
71                         "password"          : "",
72-                        "mode"              : "tcp",    #tcp,ssh,..
73+                        "proxy_password"    : "",
74+                        "mode"              : "ssh -> ssh",    #tcp,ssh,..
75                         "autoconnect"       : False,
76                         "ssh_port"          : 22,
77+                        "proxy_ssh_port"    : 22,
78                     }
79 
80 
81@@ -113,6 +126,9 @@
82     def __init__(self):
83         # Default connection options
84         self.config = make_defaults_struct(extras_defaults=LAUNCHER_DEFAULTS, extras_types=LAUNCHER_OPTION_TYPES, extras_validation=self.get_launcher_validation())
85+        ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower()
86+        self.is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
87+        self.is_paramiko = ssh_cmd=="paramiko"
88         #TODO: the fixup does not belong here?
89         from xpra.scripts.main import fixup_options
90         fixup_options(self.config)
91@@ -127,6 +143,7 @@
92 
93     def get_connection_modes(self):
94         modes = ["ssh"]
95+        modes.append("ssh -> ssh")
96         try:
97             import ssl
98             assert ssl
99@@ -221,19 +238,96 @@
100         vbox.pack_start(hbox)
101 
102         # Mode:
103-        hbox = gtk.HBox(False, 20)
104-        hbox.set_spacing(20)
105-        hbox.pack_start(gtk.Label("Mode: "))
106+        hbox = gtk.HBox(False, 5)
107         self.mode_combo = gtk.combo_box_new_text()
108         for x in self.get_connection_modes():
109             self.mode_combo.append_text(x.upper())
110         self.mode_combo.connect("changed", self.mode_changed)
111-        hbox.pack_start(self.mode_combo)
112-        vbox.pack_start(hbox)
113+        hbox.pack_start(gtk.Label("Mode: "), False, False)
114+        hbox.pack_start(self.mode_combo, False, False)
115+        align_hbox = gtk.Alignment(xalign = .5)
116+        align_hbox.add(hbox)
117+        vbox.pack_start(align_hbox)
118 
119-        # Username@Host:Port
120-        hbox = gtk.HBox(False, 0)
121-        hbox.set_spacing(5)
122+        # Username@Host:Port (ssh -> ssh, proxy)
123+        vbox_proxy = gtk.VBox(False, 15)
124+        hbox = gtk.HBox(False, 5)
125+        self.proxy_vbox = vbox_proxy
126+        self.proxy_username_entry = gtk.Entry()
127+        self.proxy_username_entry.set_max_length(128)
128+        self.proxy_username_entry.set_width_chars(16)
129+        self.proxy_username_entry.connect("changed", self.validate)
130+        self.proxy_username_entry.connect("activate", self.connect_clicked)
131+        self.proxy_username_entry.set_tooltip_text("username")
132+        self.proxy_host_entry = gtk.Entry()
133+        self.proxy_host_entry.set_max_length(128)
134+        self.proxy_host_entry.set_width_chars(24)
135+        self.proxy_host_entry.connect("changed", self.validate)
136+        self.proxy_host_entry.connect("activate", self.connect_clicked)
137+        self.proxy_host_entry.set_tooltip_text("hostname")
138+        self.proxy_ssh_port_entry = gtk.Entry()
139+        self.proxy_ssh_port_entry.set_max_length(5)
140+        self.proxy_ssh_port_entry.set_width_chars(5)
141+        self.proxy_ssh_port_entry.connect("changed", self.validate)
142+        self.proxy_ssh_port_entry.connect("activate", self.connect_clicked)
143+        self.proxy_ssh_port_entry.set_tooltip_text("SSH port")
144+        hbox.pack_start(gtk.Label("Proxy: "), False, False)
145+        hbox.pack_start(self.proxy_username_entry, True, True)
146+        hbox.pack_start(gtk.Label("@"), False, False)
147+        hbox.pack_start(self.proxy_host_entry, True, True)
148+        hbox.pack_start(self.proxy_ssh_port_entry, False, False)
149+        vbox_proxy.pack_start(hbox)
150+
151+        # Password
152+        hbox = gtk.HBox(False, 5)
153+        self.proxy_password_entry = gtk.Entry()
154+        self.proxy_password_entry.set_max_length(128)
155+        self.proxy_password_entry.set_width_chars(30)
156+        self.proxy_password_entry.set_text("")
157+        self.proxy_password_entry.set_visibility(False)
158+        self.proxy_password_entry.connect("changed", self.password_ok)
159+        self.proxy_password_entry.connect("changed", self.validate)
160+        self.proxy_password_entry.connect("activate", self.connect_clicked)
161+        self.proxy_password_label = gtk.Label("Proxy Password")
162+        hbox.pack_start(self.proxy_password_label, False, False)
163+        hbox.pack_start(self.proxy_password_entry, True, True)
164+        vbox_proxy.pack_start(hbox)
165+
166+        # Private key
167+        hbox = gtk.HBox(False,  5)
168+        self.pkey_hbox = hbox
169+        self.proxy_pkey_path_l = gtk.Label("Proxy private key path (PPK):")
170+        self.proxy_pkey_path_entry = gtk.Entry()
171+        self.proxy_pkey_path_browse = gtk.Button("Browse")
172+        self.proxy_pkey_path_browse.connect("clicked", self.proxy_pkey_path_browse_clicked)
173+        hbox.pack_start(self.proxy_pkey_path_l, False, False)
174+        hbox.pack_start(self.proxy_pkey_path_entry, True, True)
175+        hbox.pack_start(self.proxy_pkey_path_browse, False, False)
176+        vbox_proxy.pack_start(hbox)
177+
178+        # Check boxes
179+        hbox = gtk.HBox(False, 5)
180+        self.password_scb = gtk.CheckButton("Server password same as proxy")
181+        self.password_scb.set_mode(True)
182+        self.password_scb.set_active(True)
183+        self.password_scb.connect("toggled", self.validate)
184+        align_password_scb = gtk.Alignment(xalign = 1.0)
185+        align_password_scb.add(self.password_scb)
186+        self.username_scb = gtk.CheckButton("Server username same as proxy")
187+        self.username_scb.set_mode(True)
188+        self.username_scb.set_active(True)
189+        self.username_scb.connect("toggled", self.validate)
190+        align_username_scb = gtk.Alignment(xalign = 0.0)
191+        align_username_scb.add(self.username_scb)
192+        hbox.pack_start(align_username_scb, True, True)
193+        hbox.pack_start(align_password_scb, True, True)
194+        vbox_proxy.pack_start(hbox)
195+
196+        # coniditonal stuff that goes away for "normal" ssh
197+        vbox.pack_start(vbox_proxy)
198+
199+        # Username@Host:Port (main)
200+        hbox = gtk.HBox(False, 5)
201         self.username_entry = gtk.Entry()
202         self.username_entry.set_max_length(128)
203         self.username_entry.set_width_chars(16)
204@@ -240,7 +334,6 @@
205         self.username_entry.connect("changed", self.validate)
206         self.username_entry.connect("activate", self.connect_clicked)
207         self.username_entry.set_tooltip_text("username")
208-        self.username_label = gtk.Label("@")
209         self.host_entry = gtk.Entry()
210         self.host_entry.set_max_length(128)
211         self.host_entry.set_width_chars(24)
212@@ -259,18 +352,17 @@
213         self.port_entry.connect("changed", self.validate)
214         self.port_entry.connect("activate", self.connect_clicked)
215         self.port_entry.set_tooltip_text("port/display")
216-
217-        hbox.pack_start(self.username_entry)
218-        hbox.pack_start(self.username_label)
219-        hbox.pack_start(self.host_entry)
220-        hbox.pack_start(self.ssh_port_entry)
221-        hbox.pack_start(gtk.Label(":"))
222-        hbox.pack_start(self.port_entry)
223+        hbox.pack_start(gtk.Label("Server:"), False, False)
224+        hbox.pack_start(self.username_entry, True, True)
225+        hbox.pack_start(gtk.Label("@"), False, False)
226+        hbox.pack_start(self.host_entry, True, True)
227+        hbox.pack_start(self.ssh_port_entry, False, False)
228+        hbox.pack_start(gtk.Label(":"), False, False)
229+        hbox.pack_start(self.port_entry, False, False)
230         vbox.pack_start(hbox)
231 
232         # Password
233-        hbox = gtk.HBox(False, 0)
234-        hbox.set_spacing(20)
235+        hbox = gtk.HBox(False, 5)
236         self.password_entry = gtk.Entry()
237         self.password_entry.set_max_length(128)
238         self.password_entry.set_width_chars(30)
239@@ -278,14 +370,13 @@
240         self.password_entry.set_visibility(False)
241         self.password_entry.connect("changed", self.password_ok)
242         self.password_entry.connect("changed", self.validate)
243-        self.password_label = gtk.Label("Password: ")
244-        hbox.pack_start(self.password_label)
245-        hbox.pack_start(self.password_entry)
246+        self.password_label = gtk.Label("Server Password:")
247+        hbox.pack_start(self.password_label, False, False)
248+        hbox.pack_start(self.password_entry, True, True)
249         vbox.pack_start(hbox)
250 
251-        #strict host key check for SSL and SSH
252-        hbox = gtk.HBox(False, 0)
253-        hbox.set_spacing(20)
254+        # strict host key check for SSL and SSH
255+        hbox = gtk.HBox(False, 5)
256         self.nostrict_host_check = gtk.CheckButton("Disable Strict Host Key Check")
257         self.nostrict_host_check.set_active(False)
258         al = gtk.Alignment(xalign=0.5, yalign=0.5, xscale=0.0, yscale=0)
259@@ -412,10 +503,11 @@
260 
261     def validate(self, *args):
262         ssh = self.mode_combo.get_active_text()=="SSH"
263+        sshtossh = self.mode_combo.get_active_text()=="SSH -> SSH"
264         errs = []
265         host = self.host_entry.get_text()
266         errs.append((self.host_entry, not bool(host), "specify the host"))
267-        if ssh:
268+        if ssh or sshtossh:
269             #validate ssh port:
270             ssh_port = self.ssh_port_entry.get_text()
271             try:
272@@ -423,9 +515,34 @@
273             except:
274                 ssh_port = -1
275             errs.append((self.ssh_port_entry, ssh_port<0 or ssh_port>=2**16, "invalid SSH port number"))
276+        if sshtossh:
277+            #validate ssh port:
278+            proxy_ssh_port = self.proxy_ssh_port_entry.get_text()
279+            try:
280+                proxy_ssh_port = int(proxy_ssh_port)
281+            except:
282+                proxy_ssh_port = -1
283+            errs.append((self.proxy_ssh_port_entry, proxy_ssh_port<0 or proxy_ssh_port>=2**16, "invalid SSH port number"))
284         port = self.port_entry.get_text()
285-        if ssh and not port:
286-            port = 0        #port optional with ssh
287+        if sshtossh:
288+            if self.password_scb.get_active():
289+                self.password_entry.set_sensitive(False)
290+                self.password_entry.set_text(self.proxy_password_entry.get_text())
291+            else:
292+                self.password_entry.set_sensitive(True)
293+            if self.username_scb.get_active():
294+                self.username_entry.set_sensitive(False)
295+                self.username_entry.set_text(self.proxy_username_entry.get_text())
296+            else:
297+                self.username_entry.set_sensitive(True)
298+            errs.append((self.proxy_host_entry, not bool(self.proxy_host_entry.get_text()), "specify the proxy host"))
299+        # check username *after* the checkbox action
300+        if ssh or sshtossh:
301+            errs.append((self.username_entry, not bool(self.username_entry.get_text()), "specify username"))
302+        if sshtossh:
303+            errs.append((self.proxy_username_entry, not bool(self.proxy_username_entry.get_text()), "specify proxy username"))
304+        if ssh or sshtossh and not port:
305+            port = 0        # port optional with ssh
306         else:
307             try:
308                 port = int(port)
309@@ -460,14 +577,25 @@
310     def mode_changed(self, *_args):
311         mode = self.mode_combo.get_active_text().lower()
312         ssh = mode=="ssh"
313-        if ssh:
314+        sshtossh = mode=="ssh -> ssh"
315+        if ssh or sshtossh:
316             self.port_entry.set_tooltip_text("Display number (optional)")
317             self.port_entry.set_text("")
318-            self.ssh_port_entry.set_text("22")
319             self.ssh_port_entry.show()
320             self.password_entry.set_tooltip_text("SSH Password")
321             self.username_entry.set_tooltip_text("SSH Username")
322+            if ssh:
323+                self.proxy_vbox.hide()
324+                self.password_scb.hide()
325+                self.password_entry.set_sensitive(True)
326+                self.username_entry.set_sensitive(True)
327+            if sshtossh:
328+                self.proxy_vbox.show()
329+                self.password_scb.show()
330         else:
331+            self.password_entry.set_sensitive(True)
332+            self.username_entry.set_sensitive(True)
333+            self.proxy_vbox.hide()
334             self.ssh_port_entry.hide()
335             self.ssh_port_entry.set_text("")
336             port_str = self.port_entry.get_text()
337@@ -479,11 +607,13 @@
338             if self.config.port>0:
339                 self.port_entry.set_text("%s" % self.config.port)
340         can_use_password = True
341-        if ssh:
342-            ssh_cmd = parse_ssh_string(self.config.ssh)[0].strip().lower()
343-            is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
344-            is_paramiko = ssh_cmd=="paramiko"
345-            if is_putty or is_paramiko:
346+        if ssh or sshtossh:
347+            if not self.is_putty:
348+                self.proxy_pkey_path_entry.set_text("OpenSSH/Paramiko use ~/.ssh")
349+                self.proxy_pkey_path_entry.set_editable(False)
350+                self.proxy_pkey_path_entry.set_sensitive(False)
351+                self.proxy_pkey_path_browse.hide()
352+            if self.is_paramiko or self.is_putty:
353                 can_use_password = True
354             else:
355                 #we can also use password if sshpass is installed:
356@@ -493,9 +623,15 @@
357         if can_use_password:
358             self.password_label.show()
359             self.password_entry.show()
360+            if sshtossh:
361+                self.proxy_password_label.show()
362+                self.proxy_password_entry.show()
363         else:
364             self.password_label.hide()
365             self.password_entry.hide()
366+            if sshtossh:
367+                self.proxy_password_label.hide()
368+                self.proxy_password_entry.hide()
369         self.validate()
370         if mode=="ssl" or (mode=="ssh" and not WIN32):
371             self.nostrict_host_check.show()
372@@ -531,7 +667,9 @@
373     def reset_errors(self):
374         self.set_sensitive(True)
375         self.set_info_text("")
376-        for widget in (self.info, self.password_entry, self.username_entry, self.host_entry, self.port_entry):
377+        for widget in (self.info, self.password_entry, self.username_entry, self.host_entry,
378+                       self.port_entry, self.proxy_password_entry, self.proxy_username_entry,
379+                       self.proxy_host_entry, self.proxy_ssh_port_entry, self.ssh_port_entry):
380             self.set_widget_fg_color(self.info, False)
381             self.set_widget_bg_color(widget, False)
382 
383@@ -546,6 +684,21 @@
384     def set_sensitive(self, s):
385         glib.idle_add(self.window.set_sensitive, s)
386 
387+    def choose_pkey_file(self, title, action, action_button, callback):
388+        file_filter = gtk.FileFilter()
389+        file_filter.set_name("All Files")
390+        file_filter.add_pattern("*")
391+        choose_file(self.window, title, action, action_button, callback, file_filter)
392+
393+    def proxy_pkey_path_browse_clicked(self, *args):
394+        log("proxy_pkey_path_browse_clicked%s", args)
395+        def do_choose(filename):
396+            #make sure the file extension is .ppk
397+            if os.path.splitext(filename)[-1]!=".ppk":
398+                filename += ".ppk"
399+            self.proxy_pkey_path_entry.set_text(filename)
400+        self.choose_pkey_file("Choose SSH private key", FILE_CHOOSER_ACTION_OPEN, gtk.STOCK_OPEN, do_choose)
401+
402     def connect_clicked(self, *args):
403         log("connect_clicked%s", args)
404         self.update_options_from_gui()
405@@ -591,7 +744,7 @@
406         #cooked vars used by connect_to
407         params = {"type"    : self.config.mode}
408         username = self.config.username
409-        if self.config.mode=="ssh":
410+        if self.config.mode=="ssh" or self.config.mode=="ssh -> ssh":
411             if self.config.socket_dir:
412                 params["socket_dir"] = self.config.socket_dir
413             params["remote_xpra"] = self.config.remote_xpra
414@@ -602,12 +755,9 @@
415             else:
416                 params["display"] = "auto"
417                 params["display_as_args"] = []
418-            full_ssh = parse_ssh_string(self.config.ssh)
419-            ssh_cmd = full_ssh[0].lower()
420-            is_putty = ssh_cmd.endswith("plink") or ssh_cmd.endswith("plink.exe")
421-            is_paramiko = ssh_cmd=="paramiko"
422-            params["is_putty"] = is_putty
423-            params["is_paramiko"] = is_paramiko
424+            params["ssh"] = self.config.ssh
425+            params["is_putty"] = self.is_putty
426+            params["is_paramiko"] = self.is_paramiko
427             password = self.config.password
428             host = self.config.host
429             upos = host.find("@")
430@@ -622,11 +772,23 @@
431                     username = username[:ppos]
432             if self.config.ssh_port and self.config.ssh_port!=22:
433                 params["ssh-port"] = self.config.ssh_port
434-            full_ssh += add_ssh_args(username, password, host, self.config.ssh_port, is_putty, is_paramiko)
435+            ssh_cmd = parse_ssh_string(self.config.ssh)
436+            full_ssh = ssh_cmd[:]
437+            full_ssh += add_ssh_args(username, password, host, self.config.ssh_port, self.is_putty, self.is_paramiko)
438             if username:
439                 params["username"] = username
440             if self.nostrict_host_check.get_active():
441                 full_ssh += ["-o", "StrictHostKeyChecking=no"]
442+            if params["type"] == "ssh -> ssh":
443+                params["type"] = "ssh"
444+                params["proxy_host"] = self.config.proxy_host
445+                params["proxy_ssh_port"] = self.config.proxy_ssh_port
446+                params["proxy_username"] = self.config.proxy_username
447+                params["proxy_password"] = self.config.proxy_password
448+                full_ssh += add_ssh_proxy_args(self.config.proxy_username, self.config.proxy_password,
449+                                               self.config.proxy_host, self.config.proxy_ssh_port,
450+                                               self.config.proxy_pkey_path, ssh_cmd,
451+                                               self.is_putty, self.is_paramiko)
452             params["host"] = host
453             params["local"] = is_local(self.config.host)
454             params["full_ssh"] = full_ssh
455@@ -784,8 +946,15 @@
456                 return int(v)
457             except ValueError:
458                 return 0
459+        self.config.proxy_ssh_port = pint(self.proxy_ssh_port_entry.get_text())
460+        self.config.proxy_host = self.proxy_host_entry.get_text()
461+        self.config.proxy_username = self.proxy_username_entry.get_text()
462+        if self.is_putty:
463+            self.config.proxy_pkey_path = self.proxy_pkey_path_entry.get_text()
464         self.config.host = self.host_entry.get_text()
465         self.config.ssh_port = pint(self.ssh_port_entry.get_text())
466+        self.config.proxy_ssh_port = pint(self.proxy_ssh_port_entry.get_text())
467+        self.config.proxy_password = self.proxy_password_entry.get_text()
468         self.config.port = pint(self.port_entry.get_text())
469         self.config.username = self.username_entry.get_text()
470         self.config.encoding = self.get_selected_encoding() or self.config.encoding
471@@ -794,7 +963,7 @@
472             self.config.mode = "tcp"
473             if mode_enc.find("aes")>0:
474                 self.config.encryption = "AES"
475-        elif mode_enc in ("ssl", "ssh", "ws", "wss"):
476+        elif mode_enc in ("ssl", "ssh", "ws", "wss", "ssh -> ssh"):
477             self.config.mode = mode_enc
478             self.config.encryption = ""
479         self.config.password = self.password_entry.get_text()
480@@ -818,8 +987,13 @@
481             #then select it in the combo:
482             if index>=0:
483                 self.encoding_combo.set_history(index)
484+        self.proxy_username_entry.set_text(self.config.proxy_username)
485+        self.proxy_host_entry.set_text(self.config.proxy_host)
486+        if self.is_putty:
487+            self.proxy_pkey_path_entry.set_text(self.config.proxy_pkey_path)
488         self.username_entry.set_text(self.config.username)
489         self.password_entry.set_text(self.config.password)
490+        self.proxy_password_entry.set_text(self.config.proxy_password)
491         self.host_entry.set_text(self.config.host)
492         def get_port(v, default_port=""):
493             try:
494@@ -830,11 +1004,12 @@
495                 pass
496             return str(default_port)
497         dport = DEFAULT_PORT
498-        if mode=="ssh":
499+        if mode=="ssh" or mode=="ssh -> ssh":
500             #not required, so don't specify one
501             dport = ""
502         self.port_entry.set_text(get_port(self.config.port, dport))
503         self.ssh_port_entry.set_text(get_port(self.config.ssh_port))
504+        self.proxy_ssh_port_entry.set_text(get_port(self.config.proxy_ssh_port))
505 
506     def close_window(self, *_args):
507         w = self.window
508Index: xpra/net/ssh.py
509===================================================================
510--- xpra/net/ssh.py     (revision 21176)
511+++ xpra/net/ssh.py     (working copy)
512@@ -17,7 +17,7 @@
513 from xpra.platform.paths import get_xpra_command, get_ssh_known_hosts_files
514 from xpra.net.bytestreams import SocketConnection, SOCKET_TIMEOUT, ConnectionClosedException
515 from xpra.exit_codes import EXIT_SSH_KEY_FAILURE, EXIT_SSH_FAILURE
516-from xpra.os_util import bytestostr, osexpand, monotonic_time, setsid, nomodule_context, umask_context, is_WSL, WIN32, OSX, POSIX
517+from xpra.os_util import bytestostr, osexpand, monotonic_time, setsid, nomodule_context, umask_context, WIN32, OSX, POSIX
518 from xpra.util import envint, envbool, nonl, engs
519 
520 INITENV_COMMAND = os.environ.get("XPRA_INITENV_COMMAND", "xpra initenv")
521@@ -79,9 +79,8 @@
522             env["XPRA_LOG_TO_FILE"] = "0"
523             kwargs["env"] = env
524         proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, **kwargs)
525-        if is_WSL():
526-            #WSL needs to wait before calling communicate?
527-            proc.wait()
528+        # without this the return code is always None in WSL (and becomes zero after communicate())
529+#       proc.wait()
530         stdout, stderr = proc.communicate()
531         log("exec_dialog_subprocess(%s)", cmd)
532         if stderr:
533@@ -203,6 +202,9 @@
534     ipv6 = display_desc.get("ipv6", False)
535     #ssh and command attributes:
536     username = display_desc.get("username")
537+    if "proxy_host" in display_desc and not "proxy_username" in display_desc:
538+        import getpass
539+        display_desc["proxy_username"] = getpass.getuser()
540     if not username:
541         import getpass
542         username = getpass.getuser()
543@@ -252,16 +254,44 @@
544                     from paramiko.ssh_exception import ProxyCommandFailure
545                     bytestreams.CLOSED_EXCEPTIONS = tuple(list(bytestreams.CLOSED_EXCEPTIONS)+[ProxyCommandFailure])
546                     return conn
547+        from xpra.scripts.main import socket_connect
548+        from paramiko.transport import Transport
549+        from paramiko import SSHException
550+        if "proxy_host" in display_desc:
551+            proxy_host = display_desc["proxy_host"]
552+            proxy_ssh_port = display_desc.get("proxy_ssh_port", 22)
553+            proxy_username = display_desc.get("proxy_username", username)
554+            proxy_password = display_desc.get("proxy_password", password)
555+            proxy_ipv6 = display_desc.get("proxy_ipv6", False)
556+            sock = socket_connect(dtype, proxy_host, proxy_ssh_port, proxy_ipv6)
557+            middle_transport = Transport(sock)
558+            middle_transport.use_compression(False)
559+            try:
560+                middle_transport.start_client()
561+            except SSHException as e:
562+                log("start_client()", exc_info=True)
563+                raise InitException("SSH negotiation failed: %s" % e)
564+            chan_to_middle = do_ssh_paramiko_connect_to(middle_transport, proxy_host, proxy_username, proxy_password, dest_host=host, dest_port=port)
565+            transport = Transport(chan_to_middle)
566+            transport.use_compression(False)
567+            try:
568+                transport.start_client()
569+            except SSHException as e:
570+                log("start_client()", exc_info=True)
571+                raise InitException("SSH negotiation failed: %s" % e)
572+            chan = do_ssh_paramiko_connect_to(transport, host, username, password, proxy_command, remote_xpra, socket_dir, display_as_args)
573+            peername = (host, port)
574+            conn = SSHProxyCommandConnection(chan, peername, target, socket_info)
575+            conn.timeout = SOCKET_TIMEOUT
576+            conn.start_stderr_reader()
577+            return conn
578 
579         #plain TCP connection to the server,
580         #we open it then give the socket to paramiko:
581-        from xpra.scripts.main import socket_connect
582         sock = socket_connect(dtype, host, port, ipv6)
583         sockname = sock.getsockname()
584         peername = sock.getpeername()
585         log("paramiko socket_connect: sockname=%s, peername=%s", sockname, peername)
586-        from paramiko.transport import Transport
587-        from paramiko import SSHException
588         transport = Transport(sock)
589         transport.use_compression(False)
590         try:
591@@ -284,9 +314,11 @@
592     def __init__(self):
593         nomodule_context.__init__(self, "gssapi")
594 
595-
596+# (1) If the arguments after "proxy_command" are "None", then we're opening a port-forward
597+# (2) If "parachan" is set, that means we're using a port-forward
598 def do_ssh_paramiko_connect_to(transport, host, username, password,
599-                               xpra_proxy_command, remote_xpra, socket_dir, display_as_args):
600+                               xpra_proxy_command=None, remote_xpra=None, socket_dir=None, display_as_args=None,
601+                               dest_host=None, dest_port=None):
602     from paramiko import SSHException, RSAKey, PasswordRequiredException
603     from paramiko.agent import Agent
604     from paramiko.hostkeys import HostKeys
605@@ -455,6 +487,27 @@
606             log("auth_password(..)", exc_info=True)
607             log.info("SSH password authentication failed: %s", e)
608 
609+    def auth_interactive():
610+        log("trying interactive authentication")
611+        class iauthhandler:
612+            def __init__(self):
613+                self.authcount = 0
614+            def handlestuff(self, title, instructions, prompt_list):
615+                p = [ ]
616+                for pent in prompt_list:
617+                    if self.authcount == 0 and password:
618+                        p.append(password)
619+                    else:
620+                        p.append(input_pass(pent[0]))
621+                    self.authcount += 1
622+                return p
623+        try:
624+            myiauthhandler = iauthhandler()
625+            transport.auth_interactive(username, myiauthhandler.handlestuff, '')
626+        except SSHException as e:
627+            log("auth_password(..)", exc_info=True)
628+            log.info("SSH password authentication failed: %s", e)
629+
630     banner = transport.get_banner()
631     if banner:
632         log.info("SSH server banner:")
633@@ -462,12 +515,18 @@
634             log.info(" %s", x)
635 
636     log("starting authentication")
637+    # per the RFC we probably should do none first always and read off the supported
638+    # methods, however, the current code seems to work fine with OpenSSH
639     if not transport.is_authenticated() and NONE_AUTH:
640         auth_none()
641 
642+    # Some people do two-factor using KEY_AUTH to kick things off, so this happens first
643     if not transport.is_authenticated() and KEY_AUTH:
644         auth_publickey()
645 
646+    if not transport.is_authenticated() and PASSWORD_AUTH:
647+        auth_interactive()
648+
649     if not transport.is_authenticated() and PASSWORD_AUTH and password:
650         auth_password()
651 
652@@ -485,8 +544,13 @@
653 
654     if not transport.is_authenticated():
655         transport.close()
656-        raise InitException("SSH Authentication failed")
657+        # added the "host" so the user can figure out errors in ssh -> ssh mode
658+        raise InitException("SSH Authentication on " + host + " failed")
659 
660+    if(remote_xpra == None):
661+        log("Opening proxy channel")
662+        return transport.open_channel("direct-tcpip", (dest_host, dest_port), ('localhost', 0))
663+
664     assert len(remote_xpra)>0
665     log("will try to run xpra from: %s", remote_xpra)
666     for xpra_cmd in remote_xpra:
667@@ -617,6 +681,7 @@
668             kwargs["env"] = env
669         if SSH_DEBUG:
670             log.info("executing ssh command: %s" % (" ".join("\"%s\"" % x for x in cmd)))
671+       
672         child = Popen(cmd, stdin=PIPE, stdout=PIPE, **kwargs)
673     except OSError as e:
674         raise InitExit(EXIT_SSH_FAILURE, "Error running ssh command '%s': %s" % (" ".join("\"%s\"" % x for x in cmd), e))
675Index: xpra/scripts/main.py
676===================================================================
677--- xpra/scripts/main.py        (revision 21176)
678+++ xpra/scripts/main.py        (working copy)
679@@ -511,8 +511,81 @@
680         args += ["-T", host]
681     return args
682 
683+def add_ssh_proxy_args(username, password, host, ssh_port, pkey, ssh, is_putty=False, is_paramiko=False):
684+    args = []
685+    proxyline = ssh
686+    if is_putty:
687+        proxyline += ["-nc", "%host:%port"]
688+    elif not is_paramiko:
689+        proxyline += ["-W", "%h:%p"]
690+    if pkey and is_putty:
691+        # tortoise plink works with either slash, backslash needs too much escaping
692+        # because of the weird way it's passed through as a ProxyCommand
693+        proxyline += [ "-i", "\"" + pkey.replace("\\", "/") + "\""]
694+    # the double quotes are in case the password has something like "&"
695+    proxyline += add_ssh_args(username, password, host, ssh_port, is_putty, is_paramiko)
696+    if is_putty:
697+        args += ["-proxycmd", " ".join(proxyline)]
698+    elif not is_paramiko:
699+        from xpra.platform.paths import get_sshpass_command
700+        if password:
701+            sshpass_command = None
702+            sshpass_command = get_sshpass_command()
703+            if sshpass_command:
704+                proxyline.insert(0, sshpass_command)
705+                # is -e forces proxy password to match destination password
706+                proxyline.insert(1, "-e")
707+        args += ["-o", "ProxyCommand " + " ".join(proxyline)]
708+    return args
709+
710 def parse_display_name(error_cb, opts, display_name, session_name_lookup=False):
711     desc = {"display_name" : display_name}
712+    import re
713+    # Notes:
714+    # (1) this regex permits a "?" in the password or username (because not just splitting at "?").
715+    #     It doesn't look for the next  "?" until after the "@", where a "?" really indicates
716+    #     another field.
717+    # (2) all characters including "@"s go to "userpass" until the *last* "@" after which it all goes
718+    #     to "hostport"
719+    # (3) "proxy=" feature only implemented if destination is ssh://, this could be implemented for other
720+    #     destination protocols like tcp://
721+    reout = re.search("\\?proxy=(?P<p>((?P<userpass>.+)@)?(?P<hostport>[^?]+))", display_name)
722+    if reout:
723+        try:
724+            desc_tmp = dict()
725+            # This one should *always* return a host, and should end with an optional numeric port
726+            reout2 = re.match("(?P<host>[^:]+)($|:(?P<ssh_port>\d+)$)",reout.group("hostport"))
727+            if not reout2:
728+                raise RuntimeError("Bad Format")
729+            if reout2.group("host"):
730+                desc_tmp["proxy_host"] = reout2.group("host")
731+            else:
732+                raise RuntimeError("Bad Format")
733+            if reout2.group("ssh_port"):
734+                desc_tmp["proxy_ssh_port"] =  reout2.group("ssh_port")
735+            if reout.group("userpass"):
736+                # The username ends at the first colon. This decision was not unique: I could have
737+                # allowed one colon in username if there were two in the string.
738+                reout2 = re.match("(?P<username>[^:]+)(:(?P<password>.+))?", reout.group("userpass"))
739+                if not reout2:
740+                    raise RuntimeError("Bad Format")
741+                # If there is a "userpass" part, then it *must* have a username
742+                if reout2.group("username"):
743+                   desc_tmp["proxy_username"] = reout2.group("username")
744+                else:
745+                   raise RuntimeError("Bad Format")
746+                if reout2.group("password"):
747+                   desc_tmp["proxy_password"] = reout2.group("password")
748+        except RuntimeError:
749+            from xpra.log import Logger
750+            sshlog = Logger("ssh")
751+            sshlog.error("bad proxy argument: " + reout.group(0))
752+        else:
753+            # rip out the part we've processed
754+            display_name = display_name[:reout.start()] + display_name[reout.end():]
755+            # since we've got a whole package, toss it in
756+            desc.update(desc_tmp)
757+   
758     #split the display name on ":" or "/"
759     scpos = display_name.find(":")
760     slpos = display_name.find("/")
761@@ -676,7 +749,7 @@
762             host = parts[0]
763         #ie: ssh=["/usr/bin/ssh", "-v"]
764         ssh = parse_ssh_string(opts.ssh)
765-        full_ssh = ssh
766+        full_ssh = ssh[:]
767 
768         #maybe restrict to win32 only?
769         ssh_cmd = ssh[0].lower()
770@@ -698,6 +771,14 @@
771         if ssh_port and ssh_port!=22:
772             desc["ssh-port"] = ssh_port
773         full_ssh += add_ssh_args(username, password, host, ssh_port, is_putty, is_paramiko)
774+        if "proxy_host" in desc:
775+            proxy_username = desc.get("proxy_username", "")
776+            proxy_password = desc.get("proxy_password", "")
777+            proxy_host = desc["proxy_host"]
778+            proxy_ssh_port = desc.get("proxy_ssh_port", 22)
779+            proxy_pkey_path = desc.get("proxy_pkey_path", "")
780+            full_ssh += add_ssh_proxy_args(proxy_username, proxy_password, proxy_host, proxy_ssh_port,
781+                                           proxy_pkey_path, ssh, is_putty, is_paramiko)
782         desc.update({
783                      "host"     : host,
784                      "full_ssh" : full_ssh