summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ChangeLog7
-rw-r--r--channels.c76
-rw-r--r--channels.h7
-rw-r--r--clientloop.c327
-rw-r--r--clientloop.h4
-rw-r--r--defines.h5
-rw-r--r--includes.h3
-rw-r--r--readconf.c18
-rw-r--r--readconf.h5
-rw-r--r--scp.14
-rw-r--r--sftp.14
-rw-r--r--ssh-rand-helper.c6
-rw-r--r--ssh.126
-rw-r--r--ssh.c276
-rw-r--r--ssh_config.524
15 files changed, 650 insertions, 142 deletions
diff --git a/ChangeLog b/ChangeLog
index 3edf2d19b..36aeb85bb 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -24,6 +24,11 @@
[ssh.1 ssh_config.5 sshd_config.5]
List supported ciphers in man pages, tidy up ssh -c;
"looks fine" jmc@, ok markus@
+ - djm@cvs.openbsd.org 2004/06/13 15:03:02
+ [channels.c channels.h clientloop.c clientloop.h includes.h readconf.c]
+ [readconf.h scp.1 sftp.1 ssh.1 ssh.c ssh_config.5]
+ implement session multiplexing in the client (the server has supported
+ this since 2.0); ok markus@
20040603
- (dtucker) [auth-pam.c] Don't use pam_* namespace for sshd's PAM functions.
@@ -1208,4 +1213,4 @@
- (djm) Trim deprecated options from INSTALL. Mention UsePAM
- (djm) Fix quote handling in sftp; Patch from admorten AT umich.edu
-$Id: ChangeLog,v 1.3381 2004/06/15 00:30:39 djm Exp $
+$Id: ChangeLog,v 1.3382 2004/06/15 00:34:08 djm Exp $
diff --git a/channels.c b/channels.c
index 437befa34..1fb1092c8 100644
--- a/channels.c
+++ b/channels.c
@@ -39,7 +39,7 @@
*/
#include "includes.h"
-RCSID("$OpenBSD: channels.c,v 1.203 2004/05/26 23:02:39 markus Exp $");
+RCSID("$OpenBSD: channels.c,v 1.204 2004/06/13 15:03:02 djm Exp $");
#include "ssh.h"
#include "ssh1.h"
@@ -172,6 +172,7 @@ channel_register_fds(Channel *c, int rfd, int wfd, int efd,
c->rfd = rfd;
c->wfd = wfd;
c->sock = (rfd == wfd) ? rfd : -1;
+ c->ctl_fd = -1; /* XXX: set elsewhere */
c->efd = efd;
c->extended_usage = extusage;
@@ -263,6 +264,7 @@ channel_new(char *ctype, int type, int rfd, int wfd, int efd,
c->single_connection = 0;
c->detach_user = NULL;
c->confirm = NULL;
+ c->confirm_ctx = NULL;
c->input_filter = NULL;
debug("channel %d: new [%s]", found, remote_name);
return c;
@@ -304,10 +306,11 @@ channel_close_fd(int *fdp)
static void
channel_close_fds(Channel *c)
{
- debug3("channel %d: close_fds r %d w %d e %d",
- c->self, c->rfd, c->wfd, c->efd);
+ debug3("channel %d: close_fds r %d w %d e %d c %d",
+ c->self, c->rfd, c->wfd, c->efd, c->ctl_fd);
channel_close_fd(&c->sock);
+ channel_close_fd(&c->ctl_fd);
channel_close_fd(&c->rfd);
channel_close_fd(&c->wfd);
channel_close_fd(&c->efd);
@@ -333,6 +336,8 @@ channel_free(Channel *c)
if (c->sock != -1)
shutdown(c->sock, SHUT_RDWR);
+ if (c->ctl_fd != -1)
+ shutdown(c->ctl_fd, SHUT_RDWR);
channel_close_fds(c);
buffer_free(&c->input);
buffer_free(&c->output);
@@ -550,12 +555,13 @@ channel_open_message(void)
case SSH_CHANNEL_X11_OPEN:
case SSH_CHANNEL_INPUT_DRAINING:
case SSH_CHANNEL_OUTPUT_DRAINING:
- snprintf(buf, sizeof buf, " #%d %.300s (t%d r%d i%d/%d o%d/%d fd %d/%d)\r\n",
+ snprintf(buf, sizeof buf,
+ " #%d %.300s (t%d r%d i%d/%d o%d/%d fd %d/%d cfd %d)\r\n",
c->self, c->remote_name,
c->type, c->remote_id,
c->istate, buffer_len(&c->input),
c->ostate, buffer_len(&c->output),
- c->rfd, c->wfd);
+ c->rfd, c->wfd, c->ctl_fd);
buffer_append(&buffer, buf, strlen(buf));
continue;
default:
@@ -596,14 +602,14 @@ channel_request_start(int id, char *service, int wantconfirm)
logit("channel_request_start: %d: unknown channel id", id);
return;
}
- debug2("channel %d: request %s", id, service) ;
+ debug2("channel %d: request %s confirm %d", id, service, wantconfirm);
packet_start(SSH2_MSG_CHANNEL_REQUEST);
packet_put_int(c->remote_id);
packet_put_cstring(service);
packet_put_char(wantconfirm);
}
void
-channel_register_confirm(int id, channel_callback_fn *fn)
+channel_register_confirm(int id, channel_callback_fn *fn, void *ctx)
{
Channel *c = channel_lookup(id);
@@ -612,6 +618,7 @@ channel_register_confirm(int id, channel_callback_fn *fn)
return;
}
c->confirm = fn;
+ c->confirm_ctx = ctx;
}
void
channel_register_cleanup(int id, channel_callback_fn *fn)
@@ -729,6 +736,10 @@ channel_pre_open(Channel *c, fd_set * readset, fd_set * writeset)
buffer_len(&c->extended) < c->remote_window)
FD_SET(c->efd, readset);
}
+ /* XXX: What about efd? races? */
+ if (compat20 && c->ctl_fd != -1 &&
+ c->istate == CHAN_INPUT_OPEN && c->ostate == CHAN_OUTPUT_OPEN)
+ FD_SET(c->ctl_fd, readset);
}
static void
@@ -1482,6 +1493,33 @@ channel_handle_efd(Channel *c, fd_set * readset, fd_set * writeset)
return 1;
}
static int
+channel_handle_ctl(Channel *c, fd_set * readset, fd_set * writeset)
+{
+ char buf[16];
+ int len;
+
+ /* Monitor control fd to detect if the slave client exits */
+ if (c->ctl_fd != -1 && FD_ISSET(c->ctl_fd, readset)) {
+ len = read(c->ctl_fd, buf, sizeof(buf));
+ if (len < 0 && (errno == EINTR || errno == EAGAIN))
+ return 1;
+ if (len <= 0) {
+ debug2("channel %d: ctl read<=0", c->self);
+ if (c->type != SSH_CHANNEL_OPEN) {
+ debug2("channel %d: not open", c->self);
+ chan_mark_dead(c);
+ return -1;
+ } else {
+ chan_read_failed(c);
+ chan_write_failed(c);
+ }
+ return -1;
+ } else
+ fatal("%s: unexpected data on ctl fd", __func__);
+ }
+ return 1;
+}
+static int
channel_check_window(Channel *c)
{
if (c->type == SSH_CHANNEL_OPEN &&
@@ -1511,6 +1549,7 @@ channel_post_open(Channel *c, fd_set * readset, fd_set * writeset)
if (!compat20)
return;
channel_handle_efd(c, readset, writeset);
+ channel_handle_ctl(c, readset, writeset);
channel_check_window(c);
}
@@ -2011,7 +2050,7 @@ channel_input_open_confirmation(int type, u_int32_t seq, void *ctxt)
c->remote_maxpacket = packet_get_int();
if (c->confirm) {
debug2("callback start");
- c->confirm(c->self, NULL);
+ c->confirm(c->self, c->confirm_ctx);
debug2("callback done");
}
debug2("channel %d: open confirm rwindow %u rmax %u", c->self,
@@ -2531,6 +2570,27 @@ channel_connect_to(const char *host, u_short port)
return connect_to(host, port);
}
+void
+channel_send_window_changes(void)
+{
+ int i;
+ struct winsize ws;
+
+ for (i = 0; i < channels_alloc; i++) {
+ if (channels[i] == NULL ||
+ channels[i]->type != SSH_CHANNEL_OPEN)
+ continue;
+ if (ioctl(channels[i]->rfd, TIOCGWINSZ, &ws) < 0)
+ continue;
+ channel_request_start(i, "window-change", 0);
+ packet_put_int(ws.ws_col);
+ packet_put_int(ws.ws_row);
+ packet_put_int(ws.ws_xpixel);
+ packet_put_int(ws.ws_ypixel);
+ packet_send();
+ }
+}
+
/* -- X11 forwarding */
/*
diff --git a/channels.h b/channels.h
index 0a49c55ea..41f3cedd3 100644
--- a/channels.h
+++ b/channels.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: channels.h,v 1.72 2004/05/21 11:33:11 djm Exp $ */
+/* $OpenBSD: channels.h,v 1.73 2004/06/13 15:03:02 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -76,6 +76,7 @@ struct Channel {
int wfd; /* write fd */
int efd; /* extended fd */
int sock; /* sock fd */
+ int ctl_fd; /* control fd (client sharing) */
int isatty; /* rfd is a tty */
int wfd_isatty; /* wfd is a tty */
int force_drain; /* force close on iEOF */
@@ -105,6 +106,7 @@ struct Channel {
/* callback */
channel_callback_fn *confirm;
channel_callback_fn *detach_user;
+ void *confirm_ctx;
/* filter */
channel_filter_fn *input_filter;
@@ -161,10 +163,11 @@ void channel_stop_listening(void);
void channel_send_open(int);
void channel_request_start(int, char *, int);
void channel_register_cleanup(int, channel_callback_fn *);
-void channel_register_confirm(int, channel_callback_fn *);
+void channel_register_confirm(int, channel_callback_fn *, void *);
void channel_register_filter(int, channel_filter_fn *);
void channel_cancel_cleanup(int);
int channel_close_fd(int *);
+void channel_send_window_changes(void);
/* protocol handler */
diff --git a/clientloop.c b/clientloop.c
index 31e604180..6401588a9 100644
--- a/clientloop.c
+++ b/clientloop.c
@@ -59,7 +59,7 @@
*/
#include "includes.h"
-RCSID("$OpenBSD: clientloop.c,v 1.122 2004/05/22 06:32:12 djm Exp $");
+RCSID("$OpenBSD: clientloop.c,v 1.123 2004/06/13 15:03:02 djm Exp $");
#include "ssh.h"
#include "ssh1.h"
@@ -81,6 +81,9 @@ RCSID("$OpenBSD: clientloop.c,v 1.122 2004/05/22 06:32:12 djm Exp $");
#include "atomicio.h"
#include "sshpty.h"
#include "misc.h"
+#include "monitor_fdpass.h"
+#include "match.h"
+#include "msg.h"
/* import options */
extern Options options;
@@ -91,6 +94,9 @@ extern int stdin_null_flag;
/* Flag indicating that no shell has been requested */
extern int no_shell_flag;
+/* Control socket */
+extern int control_fd;
+
/*
* Name of the host we are connecting to. This is the name given on the
* command line, or the HostName specified for the user-supplied name in a
@@ -131,9 +137,19 @@ static int server_alive_timeouts = 0;
static void client_init_dispatch(void);
int session_ident = -1;
+struct confirm_ctx {
+ int want_tty;
+ int want_subsys;
+ Buffer cmd;
+ char *term;
+ struct termios tio;
+};
+
/*XXX*/
extern Kex *xxx_kex;
+void ssh_process_session2_setup(int, int, int, Buffer *);
+
/* Restores stdin to blocking mode. */
static void
@@ -291,19 +307,13 @@ client_check_window_change(void)
/** XXX race */
received_window_change_signal = 0;
- if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) < 0)
- return;
-
debug2("client_check_window_change: changed");
if (compat20) {
- channel_request_start(session_ident, "window-change", 0);
- packet_put_int(ws.ws_col);
- packet_put_int(ws.ws_row);
- packet_put_int(ws.ws_xpixel);
- packet_put_int(ws.ws_ypixel);
- packet_send();
+ channel_send_window_changes();
} else {
+ if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) < 0)
+ return;
packet_start(SSH_CMSG_WINDOW_SIZE);
packet_put_int(ws.ws_row);
packet_put_int(ws.ws_col);
@@ -335,7 +345,6 @@ server_alive_check(void)
* Waits until the client can do something (some data becomes available on
* one of the file descriptors).
*/
-
static void
client_wait_until_can_do_something(fd_set **readsetp, fd_set **writesetp,
int *maxfdp, int *nallocp, int rekeying)
@@ -381,6 +390,9 @@ client_wait_until_can_do_something(fd_set **readsetp, fd_set **writesetp,
if (packet_have_data_to_write())
FD_SET(connection_out, *writesetp);
+ if (control_fd != -1)
+ FD_SET(control_fd, *readsetp);
+
/*
* Wait for something to happen. This will suspend the process until
* some selected descriptor can be read, written, or has some other
@@ -500,6 +512,176 @@ client_process_net_input(fd_set * readset)
}
static void
+client_subsystem_reply(int type, u_int32_t seq, void *ctxt)
+{
+ int id;
+ Channel *c;
+
+ id = packet_get_int();
+ packet_check_eom();
+
+ if ((c = channel_lookup(id)) == NULL) {
+ error("%s: no channel for id %d", __func__, id);
+ return;
+ }
+
+ if (type == SSH2_MSG_CHANNEL_SUCCESS)
+ debug2("Request suceeded on channel %d", id);
+ else if (type == SSH2_MSG_CHANNEL_FAILURE) {
+ error("Request failed on channel %d", id);
+ channel_free(c);
+ }
+}
+
+static void
+client_extra_session2_setup(int id, void *arg)
+{
+ struct confirm_ctx *cctx = arg;
+ Channel *c;
+
+ if (cctx == NULL)
+ fatal("%s: cctx == NULL", __func__);
+ if ((c = channel_lookup(id)) == NULL)
+ fatal("%s: no channel for id %d", __func__, id);
+
+ client_session2_setup(id, cctx->want_tty, cctx->want_subsys,
+ cctx->term, &cctx->tio, c->rfd, &cctx->cmd,
+ client_subsystem_reply);
+
+ c->confirm_ctx = NULL;
+ buffer_free(&cctx->cmd);
+ free(cctx->term);
+ free(cctx);
+}
+
+static void
+client_process_control(fd_set * readset)
+{
+ Buffer m;
+ Channel *c;
+ int client_fd, new_fd[3], ver;
+ socklen_t addrlen;
+ struct sockaddr_storage addr;
+ struct confirm_ctx *cctx;
+ char *cmd;
+ u_int len;
+ uid_t euid;
+ gid_t egid;
+
+ /*
+ * Accept connection on control socket
+ */
+ if (control_fd == -1 || !FD_ISSET(control_fd, readset))
+ return;
+
+ memset(&addr, 0, sizeof(addr));
+ addrlen = sizeof(addr);
+ if ((client_fd = accept(control_fd,
+ (struct sockaddr*)&addr, &addrlen)) == -1) {
+ error("%s accept: %s", __func__, strerror(errno));
+ return;
+ }
+
+ if (getpeereid(client_fd, &euid, &egid) < 0) {
+ error("%s getpeereid failed: %s", __func__, strerror(errno));
+ close(client_fd);
+ return;
+ }
+ if ((euid != 0) && (getuid() != euid)) {
+ error("control mode uid mismatch: peer euid %u != uid %u",
+ (u_int) euid, (u_int) getuid());
+ close(client_fd);
+ return;
+ }
+ /* XXX: implement use of ssh-askpass to confirm additional channels */
+
+ unset_nonblock(client_fd);
+
+ buffer_init(&m);
+
+ buffer_put_int(&m, getpid());
+ if (ssh_msg_send(client_fd, /* version */0, &m) == -1) {
+ error("%s: client msg_send failed", __func__);
+ close(client_fd);
+ return;
+ }
+ buffer_clear(&m);
+
+ if (ssh_msg_recv(client_fd, &m) == -1) {
+ error("%s: client msg_recv failed", __func__);
+ close(client_fd);
+ return;
+ }
+
+ if ((ver = buffer_get_char(&m)) != 0) {
+ error("%s: wrong client version %d", __func__, ver);
+ buffer_free(&m);
+ close(client_fd);
+ return;
+ }
+
+ cctx = xmalloc(sizeof(*cctx));
+ memset(cctx, 0, sizeof(*cctx));
+
+ cctx->want_tty = buffer_get_int(&m);
+ cctx->want_subsys = buffer_get_int(&m);
+ cctx->term = buffer_get_string(&m, &len);
+
+ cmd = buffer_get_string(&m, &len);
+ buffer_init(&cctx->cmd);
+ buffer_append(&cctx->cmd, cmd, strlen(cmd));
+
+ debug2("%s: accepted tty %d, subsys %d, cmd %s", __func__,
+ cctx->want_tty, cctx->want_subsys, cmd);
+
+ /* Gather fds from client */
+ new_fd[0] = mm_receive_fd(client_fd);
+ new_fd[1] = mm_receive_fd(client_fd);
+ new_fd[2] = mm_receive_fd(client_fd);
+
+ debug2("%s: got fds stdin %d, stdout %d, stderr %d", __func__,
+ new_fd[0], new_fd[1], new_fd[2]);
+
+ /* Try to pick up ttymodes from client before it goes raw */
+ if (cctx->want_tty && tcgetattr(new_fd[0], &cctx->tio) == -1)
+ error("%s: tcgetattr: %s", __func__, strerror(errno));
+
+ buffer_clear(&m);
+ if (ssh_msg_send(client_fd, /* version */0, &m) == -1) {
+ error("%s: client msg_send failed", __func__);
+ close(client_fd);
+ close(new_fd[0]);
+ close(new_fd[1]);
+ close(new_fd[2]);
+ return;
+ }
+ buffer_free(&m);
+
+ /* enable nonblocking unless tty */
+ if (!isatty(new_fd[0]))
+ set_nonblock(new_fd[0]);
+ if (!isatty(new_fd[1]))
+ set_nonblock(new_fd[1]);
+ if (!isatty(new_fd[2]))
+ set_nonblock(new_fd[2]);
+
+ set_nonblock(client_fd);
+
+ c = channel_new("session", SSH_CHANNEL_OPENING,
+ new_fd[0], new_fd[1], new_fd[2],
+ CHAN_SES_WINDOW_DEFAULT, CHAN_SES_PACKET_DEFAULT,
+ CHAN_EXTENDED_WRITE, "client-session", /*nonblock*/0);
+
+ /* XXX */
+ c->ctl_fd = client_fd;
+
+ debug3("%s: channel_new: %d", __func__, c->self);
+
+ channel_send_open(c->self);
+ channel_register_confirm(c->self, client_extra_session2_setup, cctx);
+}
+
+static void
process_cmdline(void)
{
void (*handler)(int);
@@ -901,9 +1083,6 @@ simple_escape_filter(Channel *c, char *buf, int len)
static void
client_channel_closed(int id, void *arg)
{
- if (id != session_ident)
- error("client_channel_closed: id %d != session_ident %d",
- id, session_ident);
channel_cancel_cleanup(id);
session_closed = 1;
leave_raw_mode();
@@ -937,6 +1116,8 @@ client_loop(int have_pty, int escape_char_arg, int ssh2_chan_id)
connection_in = packet_get_connection_in();
connection_out = packet_get_connection_out();
max_fd = MAX(connection_in, connection_out);
+ if (control_fd != -1)
+ max_fd = MAX(max_fd, control_fd);
if (!compat20) {
/* enable nonblocking unless tty */
@@ -1054,6 +1235,9 @@ client_loop(int have_pty, int escape_char_arg, int ssh2_chan_id)
/* Buffer input from the connection. */
client_process_net_input(readset);
+ /* Accept control connections. */
+ client_process_control(readset);
+
if (quit_pending)
break;
@@ -1385,7 +1569,7 @@ static void
client_input_channel_req(int type, u_int32_t seq, void *ctxt)
{
Channel *c = NULL;
- int id, reply, success = 0;
+ int exitval, id, reply, success = 0;
char *rtype;
id = packet_get_int();
@@ -1395,18 +1579,21 @@ client_input_channel_req(int type, u_int32_t seq, void *ctxt)
debug("client_input_channel_req: channel %d rtype %s reply %d",
id, rtype, reply);
- if (session_ident == -1) {
- error("client_input_channel_req: no channel %d", session_ident);
- } else if (id != session_ident) {
- error("client_input_channel_req: channel %d: wrong channel: %d",
- session_ident, id);
- }
c = channel_lookup(id);
if (c == NULL) {
error("client_input_channel_req: channel %d: unknown channel", id);
} else if (strcmp(rtype, "exit-status") == 0) {
- success = 1;
- exit_status = packet_get_int();
+ exitval = packet_get_int();
+ if (id == session_ident) {
+ success = 1;
+ exit_status = exitval;
+ } else if (c->ctl_fd == -1) {
+ error("client_input_channel_req: unexpected channel %d",
+ session_ident);
+ } else {
+ atomicio(vwrite, c->ctl_fd, &exitval, sizeof(exitval));
+ success = 1;
+ }
packet_check_eom();
}
if (reply) {
@@ -1437,6 +1624,98 @@ client_input_global_request(int type, u_int32_t seq, void *ctxt)
xfree(rtype);
}
+void
+client_session2_setup(int id, int want_tty, int want_subsystem,
+ const char *term, struct termios *tiop, int in_fd, Buffer *cmd,
+ dispatch_fn *subsys_repl)
+{
+ int len;
+
+ debug2("%s: id %d", __func__, id);
+
+ if (want_tty) {
+ struct winsize ws;
+ struct termios tio;
+
+ /* Store window size in the packet. */
+ if (ioctl(in_fd, TIOCGWINSZ, &ws) < 0)
+ memset(&ws, 0, sizeof(ws));
+
+ channel_request_start(id, "pty-req", 0);
+ packet_put_cstring(term != NULL ? term : "");
+ packet_put_int(ws.ws_col);
+ packet_put_int(ws.ws_row);
+ packet_put_int(ws.ws_xpixel);
+ packet_put_int(ws.ws_ypixel);
+ tio = get_saved_tio();
+ tty_make_modes(-1, tiop != NULL ? tiop : &tio);
+ packet_send();
+ /* XXX wait for reply */
+ }
+
+ /* Transfer any environment variables from client to server */
+ if (options.num_send_env != 0) {
+ int i, j, matched;
+ extern char **environ;
+ char *name, *val;
+
+ debug("Sending environment.");
+ for (i = 0; environ && environ[i] != NULL; i++) {
+ /* Split */
+ name = xstrdup(environ[i]);
+ if ((val = strchr(name, '=')) == NULL) {
+ free(name);
+ continue;
+ }
+ *val++ = '\0';
+
+ matched = 0;
+ for (j = 0; j < options.num_send_env; j++) {
+ if (match_pattern(name, options.send_env[j])) {
+ matched = 1;
+ break;
+ }
+ }
+ if (!matched) {
+ debug3("Ignored env %s", name);
+ free(name);
+ continue;
+ }
+
+ debug("Sending env %s = %s", name, val);
+ channel_request_start(id, "env", 0);
+ packet_put_cstring(name);
+ packet_put_cstring(val);
+ packet_send();
+ free(name);
+ }
+ }
+
+ len = buffer_len(cmd);
+ if (len > 0) {
+ if (len > 900)
+ len = 900;
+ if (want_subsystem) {
+ debug("Sending subsystem: %.*s", len, (u_char*)buffer_ptr(cmd));
+ channel_request_start(id, "subsystem", subsys_repl != NULL);
+ if (subsys_repl != NULL) {
+ /* register callback for reply */
+ /* XXX we assume that client_loop has already been called */
+ dispatch_set(SSH2_MSG_CHANNEL_FAILURE, subsys_repl);
+ dispatch_set(SSH2_MSG_CHANNEL_SUCCESS, subsys_repl);
+ }
+ } else {
+ debug("Sending command: %.*s", len, (u_char*)buffer_ptr(cmd));
+ channel_request_start(id, "exec", 0);
+ }
+ packet_put_string(buffer_ptr(cmd), buffer_len(cmd));
+ packet_send();
+ } else {
+ channel_request_start(id, "shell", 0);
+ packet_send();
+ }
+}
+
static void
client_init_dispatch_20(void)
{
@@ -1503,5 +1782,7 @@ cleanup_exit(int i)
{
leave_raw_mode();
leave_non_blocking();
+ if (options.control_path != NULL && control_fd != -1)
+ unlink(options.control_path);
_exit(i);
}
diff --git a/clientloop.h b/clientloop.h
index 56af06bc1..f1e13ac3a 100644
--- a/clientloop.h
+++ b/clientloop.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: clientloop.h,v 1.8 2003/12/16 15:49:51 markus Exp $ */
+/* $OpenBSD: clientloop.h,v 1.9 2004/06/13 15:03:02 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -38,3 +38,5 @@
/* Client side main loop for the interactive session. */
int client_loop(int, int, int);
void client_global_request_reply_fwd(int, u_int32_t, void *);
+void client_session2_setup(int, int, int, const char *, struct termios *,
+ int, Buffer *, dispatch_fn *);
diff --git a/defines.h b/defines.h
index 9b72afecb..73a45fe44 100644
--- a/defines.h
+++ b/defines.h
@@ -25,7 +25,7 @@
#ifndef _DEFINES_H
#define _DEFINES_H
-/* $Id: defines.h,v 1.115 2004/04/14 07:24:30 dtucker Exp $ */
+/* $Id: defines.h,v 1.116 2004/06/15 00:34:08 djm Exp $ */
/* Constants */
@@ -462,6 +462,9 @@ struct winsize {
(struct cmsghdr *)NULL)
#endif /* CMSG_FIRSTHDR */
+#ifndef offsetof
+# define offsetof(type, member) ((size_t) &((type *)0)->member)
+#endif
/* Function replacement / compatibility hacks */
diff --git a/includes.h b/includes.h
index ca943c7e6..99b70502c 100644
--- a/includes.h
+++ b/includes.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: includes.h,v 1.17 2002/01/26 16:44:22 stevesk Exp $ */
+/* $OpenBSD: includes.h,v 1.18 2004/06/13 15:03:02 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -33,6 +33,7 @@ static /**/const char *const rcsid[] = { (char *)rcsid, "\100(#)" msg }
#include <grp.h>
#include <time.h>
#include <dirent.h>
+#include <stddef.h>
#ifdef HAVE_LIMITS_H
# include <limits.h> /* For PATH_MAX */
diff --git a/readconf.c b/readconf.c
index 5aa371ed9..2b1d7cc46 100644
--- a/readconf.c
+++ b/readconf.c
@@ -12,7 +12,7 @@
*/
#include "includes.h"
-RCSID("$OpenBSD: readconf.c,v 1.131 2004/05/27 00:50:13 dtucker Exp $");
+RCSID("$OpenBSD: readconf.c,v 1.132 2004/06/13 15:03:02 djm Exp $");
#include "ssh.h"
#include "xmalloc.h"
@@ -106,7 +106,7 @@ typedef enum {
oEnableSSHKeysign, oRekeyLimit, oVerifyHostKeyDNS, oConnectTimeout,
oAddressFamily, oGssAuthentication, oGssDelegateCreds,
oServerAliveInterval, oServerAliveCountMax, oIdentitiesOnly,
- oSendEnv,
+ oSendEnv, oControlPath, oControlMaster,
oDeprecated, oUnsupported
} OpCodes;
@@ -195,6 +195,8 @@ static struct {
{ "serveraliveinterval", oServerAliveInterval },
{ "serveralivecountmax", oServerAliveCountMax },
{ "sendenv", oSendEnv },
+ { "controlpath", oControlPath },
+ { "controlmaster", oControlMaster },
{ NULL, oBadOption }
};
@@ -764,6 +766,14 @@ parse_int:
}
break;
+ case oControlPath:
+ charptr = &options->control_path;
+ goto parse_string;
+
+ case oControlMaster:
+ intptr = &options->control_master;
+ goto parse_flag;
+
case oDeprecated:
debug("%s line %d: Deprecated option \"%s\"",
filename, linenum, keyword);
@@ -905,6 +915,8 @@ initialize_options(Options * options)
options->server_alive_interval = -1;
options->server_alive_count_max = -1;
options->num_send_env = 0;
+ options->control_path = NULL;
+ options->control_master = -1;
}
/*
@@ -1025,6 +1037,8 @@ fill_default_options(Options * options)
options->server_alive_interval = 0;
if (options->server_alive_count_max == -1)
options->server_alive_count_max = 3;
+ if (options->control_master == -1)
+ options->control_master = 0;
/* options->proxy_command should not be set by default */
/* options->user will be set in the main program if appropriate */
/* options->hostname will be set in the main program if appropriate */
diff --git a/readconf.h b/readconf.h
index 668055943..5e504bece 100644
--- a/readconf.h
+++ b/readconf.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: readconf.h,v 1.62 2004/04/27 09:46:37 djm Exp $ */
+/* $OpenBSD: readconf.h,v 1.63 2004/06/13 15:03:02 djm Exp $ */
/*
* Author: Tatu Ylonen <ylo@cs.hut.fi>
@@ -108,6 +108,9 @@ typedef struct {
int num_send_env;
char *send_env[MAX_SEND_ENV];
+
+ char *control_path;
+ int control_master;
} Options;
diff --git a/scp.1 b/scp.1
index 202ebaadb..f346b2ae9 100644
--- a/scp.1
+++ b/scp.1
@@ -9,7 +9,7 @@
.\"
.\" Created: Sun May 7 00:14:37 1995 ylo
.\"
-.\" $OpenBSD: scp.1,v 1.35 2004/05/04 18:36:07 jmc Exp $
+.\" $OpenBSD: scp.1,v 1.36 2004/06/13 15:03:02 djm Exp $
.\"
.Dd September 25, 1999
.Dt SCP 1
@@ -128,6 +128,8 @@ For full details of the options listed below, and their possible values, see
.It CompressionLevel
.It ConnectionAttempts
.It ConnectTimeout
+.It ControlMaster
+.It ControlPath
.It GlobalKnownHostsFile
.It GSSAPIAuthentication
.It GSSAPIDelegateCredentials
diff --git a/sftp.1 b/sftp.1
index 795a0342f..7f0ef1121 100644
--- a/sftp.1
+++ b/sftp.1
@@ -1,4 +1,4 @@
-.\" $OpenBSD: sftp.1,v 1.54 2004/05/02 23:02:17 dtucker Exp $
+.\" $OpenBSD: sftp.1,v 1.55 2004/06/13 15:03:02 djm Exp $
.\"
.\" Copyright (c) 2001 Damien Miller. All rights reserved.
.\"
@@ -154,6 +154,8 @@ For full details of the options listed below, and their possible values, see
.It CompressionLevel
.It ConnectionAttempts
.It ConnectTimeout
+.It ControlMaster
+.It ControlPath
.It GlobalKnownHostsFile
.It GSSAPIAuthentication
.It GSSAPIDelegateCredentials
diff --git a/ssh-rand-helper.c b/ssh-rand-helper.c
index 8a320a71e..471e7295b 100644
--- a/ssh-rand-helper.c
+++ b/ssh-rand-helper.c
@@ -39,7 +39,7 @@
#include "pathnames.h"
#include "log.h"
-RCSID("$Id: ssh-rand-helper.c,v 1.16 2003/11/21 12:56:47 djm Exp $");
+RCSID("$Id: ssh-rand-helper.c,v 1.17 2004/06/15 00:34:08 djm Exp $");
/* Number of bytes we write out */
#define OUTPUT_SEED_SIZE 48
@@ -69,10 +69,6 @@ extern char *__progname;
char *__progname;
#endif
-#ifndef offsetof
-# define offsetof(type, member) ((size_t) &((type *)0)->member)
-#endif
-
#define WHITESPACE " \t\n"
#ifndef RUSAGE_SELF
diff --git a/ssh.1 b/ssh.1
index 6cef0851d..b70102be5 100644
--- a/ssh.1
+++ b/ssh.1
@@ -34,7 +34,7 @@
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.\"
-.\" $OpenBSD: ssh.1,v 1.189 2004/06/13 14:01:42 dtucker Exp $
+.\" $OpenBSD: ssh.1,v 1.190 2004/06/13 15:03:02 djm Exp $
.Dd September 25, 1999
.Dt SSH 1
.Os
@@ -43,7 +43,7 @@
.Nd OpenSSH SSH client (remote login program)
.Sh SYNOPSIS
.Nm ssh
-.Op Fl 1246AaCfgkNnqsTtVvXxY
+.Op Fl 1246AaCfgkMNnqSsTtVvXxY
.Op Fl b Ar bind_address
.Op Fl c Ar cipher_spec
.Op Fl D Ar port
@@ -605,6 +605,17 @@ be specified in order of preference.
See the
.Cm MACs
keyword for more information.
+.It Fl M
+Places the
+.Nm
+client into
+.Dq master
+mode for connection sharing.
+Refer to the description of
+.Cm ControlMaster
+in
+.Xr ssh_config 5
+for details.
.It Fl N
Do not execute a remote command.
This is useful for just forwarding ports
@@ -649,6 +660,8 @@ For full details of the options listed below, and their possible values, see
.It CompressionLevel
.It ConnectionAttempts
.It ConnectTimeout
+.It ControlMaster
+.It ControlPath
.It DynamicForward
.It EscapeChar
.It ForwardAgent
@@ -724,6 +737,15 @@ IPv6 addresses can be specified with an alternative syntax:
.Ar hostport .
.Xc
.Sm on
+.It Fl S
+Places the
+.Nm
+client into slave mode for connection sharing.
+Refer to the description of
+.Cm ControlMaster
+in
+.Xr ssh_config 5
+for details.
.It Fl s
May be used to request invocation of a subsystem on the remote system.
Subsystems are a feature of the SSH2 protocol which facilitate the use
diff --git a/ssh.c b/ssh.c
index 3c21fa37d..1c6ec8b6a 100644
--- a/ssh.c
+++ b/ssh.c
@@ -40,7 +40,7 @@
*/
#include "includes.h"
-RCSID("$OpenBSD: ssh.c,v 1.213 2004/05/08 00:01:37 deraadt Exp $");
+RCSID("$OpenBSD: ssh.c,v 1.214 2004/06/13 15:03:02 djm Exp $");
#include <openssl/evp.h>
#include <openssl/err.h>
@@ -53,21 +53,24 @@ RCSID("$OpenBSD: ssh.c,v 1.213 2004/05/08 00:01:37 deraadt Exp $");
#include "xmalloc.h"
#include "packet.h"
#include "buffer.h"
+#include "bufaux.h"
#include "channels.h"
#include "key.h"
#include "authfd.h"
#include "authfile.h"
#include "pathnames.h"
+#include "dispatch.h"
#include "clientloop.h"
#include "log.h"
#include "readconf.h"
#include "sshconnect.h"
-#include "dispatch.h"
#include "misc.h"
#include "kex.h"
#include "mac.h"
#include "sshpty.h"
#include "match.h"
+#include "msg.h"
+#include "monitor_fdpass.h"
#ifdef SMARTCARD
#include "scard.h"
@@ -141,6 +144,13 @@ static int client_global_request_id = 0;
/* pid of proxycommand child process */
pid_t proxy_command_pid = 0;
+/* fd to control socket */
+int control_fd = -1;
+
+/* Only used in control client mode */
+volatile sig_atomic_t control_client_terminate = 0;
+u_int control_server_pid = 0;
+
/* Prints a help message to the user. This function never returns. */
static void
@@ -158,6 +168,7 @@ usage(void)
static int ssh_session(void);
static int ssh_session2(void);
static void load_public_identity_files(void);
+static void control_client(const char *path);
/*
* Main program for the ssh client.
@@ -228,7 +239,7 @@ main(int ac, char **av)
again:
while ((opt = getopt(ac, av,
- "1246ab:c:e:fgi:kl:m:no:p:qstvxACD:F:I:L:NPR:TVXY")) != -1) {
+ "1246ab:c:e:fgi:kl:m:no:p:qstvxACD:F:I:L:MNPR:S:TVXY")) != -1) {
switch (opt) {
case '1':
options.protocol = SSH_PROTO_1;
@@ -364,6 +375,9 @@ again:
exit(1);
}
break;
+ case 'M':
+ options.control_master = 1;
+ break;
case 'p':
options.port = a2port(optarg);
if (options.port == 0) {
@@ -432,6 +446,13 @@ again:
case 's':
subsystem_flag = 1;
break;
+ case 'S':
+ if (options.control_path != NULL)
+ free(options.control_path);
+ options.control_path = xstrdup(optarg);
+ if (options.control_master == -1)
+ options.control_master = 0;
+ break;
case 'b':
options.bind_address = optarg;
break;
@@ -566,6 +587,13 @@ again:
strcmp(options.proxy_command, "none") == 0)
options.proxy_command = NULL;
+ if (options.control_path != NULL) {
+ options.control_path = tilde_expand_filename(
+ options.control_path, original_real_uid);
+ }
+ if (options.control_path != NULL && options.control_master == 0)
+ control_client(options.control_path); /* This doesn't return */
+
/* Open a connection to the remote host. */
if (ssh_connect(host, &hostaddr, options.port,
options.address_family, options.connection_attempts,
@@ -678,6 +706,9 @@ again:
exit_status = compat20 ? ssh_session2() : ssh_session();
packet_close();
+ if (options.control_path != NULL && control_fd != -1)
+ unlink(options.control_path);
+
/*
* Send SIGHUP to proxy command if used. We don't wait() in
* case it hangs and instead rely on init to reap the child
@@ -974,7 +1005,7 @@ ssh_session(void)
}
static void
-client_subsystem_reply(int type, u_int32_t seq, void *ctxt)
+ssh_subsystem_reply(int type, u_int32_t seq, void *ctxt)
{
int id, len;
@@ -1006,40 +1037,50 @@ client_global_request_reply_fwd(int type, u_int32_t seq, void *ctxt)
options.remote_forwards[i].port);
}
-/* request pty/x11/agent/tcpfwd/shell for channel */
static void
-ssh_session2_setup(int id, void *arg)
+ssh_control_listener(void)
{
- int len;
- int interactive = 0;
- struct termios tio;
+ struct sockaddr_un addr;
+ mode_t old_umask;
+
+ if (options.control_path == NULL || options.control_master != 1)
+ return;
- debug2("ssh_session2_setup: id %d", id);
+ memset(&addr, '\0', sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ addr.sun_len = offsetof(struct sockaddr_un, sun_path) +
+ strlen(options.control_path) + 1;
- if (tty_flag) {
- struct winsize ws;
- char *cp;
- cp = getenv("TERM");
- if (!cp)
- cp = "";
- /* Store window size in the packet. */
- if (ioctl(fileno(stdin), TIOCGWINSZ, &ws) < 0)
- memset(&ws, 0, sizeof(ws));
+ if (strlcpy(addr.sun_path, options.control_path,
+ sizeof(addr.sun_path)) >= sizeof(addr.sun_path))
+ fatal("ControlPath too long");
- channel_request_start(id, "pty-req", 0);
- packet_put_cstring(cp);
- packet_put_int(ws.ws_col);
- packet_put_int(ws.ws_row);
- packet_put_int(ws.ws_xpixel);
- packet_put_int(ws.ws_ypixel);
- tio = get_saved_tio();
- tty_make_modes(/*ignored*/ 0, &tio);
- packet_send();
- interactive = 1;
- /* XXX wait for reply */
+ if ((control_fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
+ fatal("%s socket(): %s\n", __func__, strerror(errno));
+
+ old_umask = umask(0177);
+ if (bind(control_fd, (struct sockaddr*)&addr, addr.sun_len) == -1) {
+ control_fd = -1;
+ if (errno == EINVAL)
+ fatal("ControlSocket %s already exists",
+ options.control_path);
+ else
+ fatal("%s bind(): %s\n", __func__, strerror(errno));
}
- if (options.forward_x11 &&
- getenv("DISPLAY") != NULL) {
+ umask(old_umask);
+
+ if (listen(control_fd, 64) == -1)
+ fatal("%s listen(): %s\n", __func__, strerror(errno));
+
+ set_nonblock(control_fd);
+}
+
+/* request pty/x11/agent/tcpfwd/shell for channel */
+static void
+ssh_session2_setup(int id, void *arg)
+{
+ int interactive = tty_flag;
+ if (options.forward_x11 && getenv("DISPLAY") != NULL) {
char *proto, *data;
/* Get reasonable local authentication information. */
x11_get_proto(&proto, &data);
@@ -1057,65 +1098,8 @@ ssh_session2_setup(int id, void *arg)
packet_send();
}
- /* Transfer any environment variables from client to server */
- if (options.num_send_env != 0) {
- int i, j, matched;
- extern char **environ;
- char *name, *val;
-
- debug("Sending environment.");
- for (i = 0; environ && environ[i] != NULL; i++) {
- /* Split */
- name = xstrdup(environ[i]);
- if ((val = strchr(name, '=')) == NULL) {
- free(name);
- continue;
- }
- *val++ = '\0';
-
- matched = 0;
- for (j = 0; j < options.num_send_env; j++) {
- if (match_pattern(name, options.send_env[j])) {
- matched = 1;
- break;
- }
- }
- if (!matched) {
- debug3("Ignored env %s", name);
- free(name);
- continue;
- }
-
- debug("Sending env %s = %s", name, val);
- channel_request_start(id, "env", 0);
- packet_put_cstring(name);
- packet_put_cstring(val);
- packet_send();
- free(name);
- }
- }
-
- len = buffer_len(&command);
- if (len > 0) {
- if (len > 900)
- len = 900;
- if (subsystem_flag) {
- debug("Sending subsystem: %.*s", len, (u_char *)buffer_ptr(&command));
- channel_request_start(id, "subsystem", /*want reply*/ 1);
- /* register callback for reply */
- /* XXX we assume that client_loop has already been called */
- dispatch_set(SSH2_MSG_CHANNEL_FAILURE, &client_subsystem_reply);
- dispatch_set(SSH2_MSG_CHANNEL_SUCCESS, &client_subsystem_reply);
- } else {
- debug("Sending command: %.*s", len, (u_char *)buffer_ptr(&command));
- channel_request_start(id, "exec", 0);
- }
- packet_put_string(buffer_ptr(&command), buffer_len(&command));
- packet_send();
- } else {
- channel_request_start(id, "shell", 0);
- packet_send();
- }
+ client_session2_setup(id, tty_flag, subsystem_flag, getenv("TERM"),
+ NULL, fileno(stdin), &command, &ssh_subsystem_reply);
packet_set_interactive(interactive);
}
@@ -1161,7 +1145,7 @@ ssh_session2_open(void)
channel_send_open(c->self);
if (!no_shell_flag)
- channel_register_confirm(c->self, ssh_session2_setup);
+ channel_register_confirm(c->self, ssh_session2_setup, NULL);
return c->self;
}
@@ -1173,6 +1157,7 @@ ssh_session2(void)
/* XXX should be pre-session */
ssh_init_forwarding();
+ ssh_control_listener();
if (!no_shell_flag || (datafellows & SSH_BUG_DUMMYCHAN))
id = ssh_session2_open();
@@ -1226,3 +1211,110 @@ load_public_identity_files(void)
options.identity_keys[i] = public;
}
}
+
+static void
+control_client_sighandler(int signo)
+{
+ control_client_terminate = signo;
+}
+
+static void
+control_client_sigrelay(int signo)
+{
+ if (control_server_pid > 1)
+ kill(control_server_pid, signo);
+}
+
+static void
+control_client(const char *path)
+{
+ struct sockaddr_un addr;
+ int r, sock, exitval;
+ Buffer m;
+ char *cp;
+
+ memset(&addr, '\0', sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ addr.sun_len = offsetof(struct sockaddr_un, sun_path) +
+ strlen(path) + 1;
+
+ if (strlcpy(addr.sun_path, path,
+ sizeof(addr.sun_path)) >= sizeof(addr.sun_path))
+ fatal("ControlPath too long");
+
+ if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
+ fatal("%s socket(): %s", __func__, strerror(errno));
+
+ if (connect(sock, (struct sockaddr*)&addr, addr.sun_len) == -1)
+ fatal("Couldn't connect to %s: %s", path, strerror(errno));
+
+ if ((cp = getenv("TERM")) == NULL)
+ cp = "";
+
+ signal(SIGINT, control_client_sighandler);
+ signal(SIGTERM, control_client_sighandler);
+ signal(SIGWINCH, control_client_sigrelay);
+
+ buffer_init(&m);
+
+ /* Get PID of controlee */
+ if (ssh_msg_recv(sock, &m) == -1)
+ fatal("%s: msg_recv", __func__);
+ if (buffer_get_char(&m) != 0)
+ fatal("%s: wrong version", __func__);
+ control_server_pid = buffer_get_int(&m);
+
+ /* XXX: env passing */
+
+ buffer_clear(&m);
+ buffer_put_int(&m, tty_flag);
+ buffer_put_int(&m, subsystem_flag);
+ buffer_put_cstring(&m, cp);
+
+ buffer_append(&command, "\0", 1);
+ buffer_put_cstring(&m, buffer_ptr(&command));
+
+ if (ssh_msg_send(sock, /* version */0, &m) == -1)
+ fatal("%s: msg_send", __func__);
+
+ mm_send_fd(sock, STDIN_FILENO);
+ mm_send_fd(sock, STDOUT_FILENO);
+ mm_send_fd(sock, STDERR_FILENO);
+
+ /* Wait for reply, so master has a chance to gather ttymodes */
+ buffer_clear(&m);
+ if (ssh_msg_recv(sock, &m) == -1)
+ fatal("%s: msg_recv", __func__);
+ if (buffer_get_char(&m) != 0)
+ fatal("%s: master returned error", __func__);
+ buffer_free(&m);
+
+ if (tty_flag)
+ enter_raw_mode();
+
+ /* Stick around until the controlee closes the client_fd */
+ exitval = 0;
+ for (;!control_client_terminate;) {
+ r = read(sock, &exitval, sizeof(exitval));
+ if (r == 0) {
+ debug2("Received EOF from master");
+ break;
+ }
+ if (r > 0)
+ debug2("Received exit status from master %d", exitval);
+ if (r == -1 && errno != EINTR)
+ fatal("%s: read %s", __func__, strerror(errno));
+ }
+
+ if (control_client_terminate)
+ debug2("Exiting on signal %d", control_client_terminate);
+
+ close(sock);
+
+ leave_raw_mode();
+
+ if (tty_flag && options.log_level != SYSLOG_LEVEL_QUIET)
+ fprintf(stderr, "Connection to master closed.\r\n");
+
+ exit(exitval);
+}
diff --git a/ssh_config.5 b/ssh_config.5
index 46d3012c8..bab11d313 100644
--- a/ssh_config.5
+++ b/ssh_config.5
@@ -34,7 +34,7 @@
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.\"
-.\" $OpenBSD: ssh_config.5,v 1.35 2004/06/13 14:01:42 dtucker Exp $
+.\" $OpenBSD: ssh_config.5,v 1.36 2004/06/13 15:03:02 djm Exp $
.Dd September 25, 1999
.Dt SSH_CONFIG 5
.Os
@@ -256,6 +256,28 @@ will act as a SOCKS server.
Multiple forwardings may be specified, and
additional forwardings can be given on the command line.
Only the superuser can forward privileged ports.
+.It Cm ControlMaster
+Enables the sharing of multiple sessions over a single network connection.
+When set to
+.Dq yes
+.Nm ssh
+will listen for connections on a control socket specified using the
+.Cm ControlPath
+argument.
+Additional sessions can connect to this socket using the same
+.Cm ControlPath
+with
+.Cm ControlMaster
+set to
+.Dq no
+(the default.)
+These sessions will reuse the master instance's network connection rather
+than initiating new ones.
+.It Cm ControlPath
+Specify a the path to the control socket used for connection sharing.
+See
+.Cm ControlMaster
+above.
.It Cm EnableSSHKeysign
Setting this option to
.Dq yes