upstream: Make with config keywords support which
percent_expansions more consistent. - %C is moved into its own function and added to Match Exec. - move the common (global) options into a macro. This is ugly but it's the least-ugly way I could come up with. - move IdentityAgent and ForwardAgent percent expansion to before the config dump to make it regression-testable. - document all of the above ok jmc@ for man page bits, "makes things less terrible" djm@ for the rest. OpenBSD-Commit-ID: 4b65664bd6d8ae2a9afaf1a2438ddd1b614b1d75
This commit is contained in:
parent
6ec7457171
commit
ed833da176
26
readconf.c
26
readconf.c
|
@ -1,4 +1,4 @@
|
||||||
/* $OpenBSD: readconf.c,v 1.326 2020/02/06 22:46:31 djm Exp $ */
|
/* $OpenBSD: readconf.c,v 1.327 2020/04/03 02:27:12 dtucker Exp $ */
|
||||||
/*
|
/*
|
||||||
* Author: Tatu Ylonen <ylo@cs.hut.fi>
|
* Author: Tatu Ylonen <ylo@cs.hut.fi>
|
||||||
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
|
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
|
||||||
|
@ -324,6 +324,24 @@ kex_default_pk_alg(void)
|
||||||
return kex_default_pk_alg_filtered;
|
return kex_default_pk_alg_filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char *
|
||||||
|
ssh_connection_hash(const char *thishost, const char *host, const char *portstr,
|
||||||
|
const char *user)
|
||||||
|
{
|
||||||
|
struct ssh_digest_ctx *md;
|
||||||
|
u_char conn_hash[SSH_DIGEST_MAX_LENGTH];
|
||||||
|
|
||||||
|
if ((md = ssh_digest_start(SSH_DIGEST_SHA1)) == NULL ||
|
||||||
|
ssh_digest_update(md, thishost, strlen(thishost)) < 0 ||
|
||||||
|
ssh_digest_update(md, host, strlen(host)) < 0 ||
|
||||||
|
ssh_digest_update(md, portstr, strlen(portstr)) < 0 ||
|
||||||
|
ssh_digest_update(md, user, strlen(user)) < 0 ||
|
||||||
|
ssh_digest_final(md, conn_hash, sizeof(conn_hash)) < 0)
|
||||||
|
fatal("%s: mux digest failed", __func__);
|
||||||
|
ssh_digest_free(md);
|
||||||
|
return tohex(conn_hash, ssh_digest_bytes(SSH_DIGEST_SHA1));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Adds a local TCP/IP port forward to options. Never returns if there is an
|
* Adds a local TCP/IP port forward to options. Never returns if there is an
|
||||||
* error.
|
* error.
|
||||||
|
@ -646,6 +664,8 @@ match_cfg_line(Options *options, char **condition, struct passwd *pw,
|
||||||
if (r == (negate ? 1 : 0))
|
if (r == (negate ? 1 : 0))
|
||||||
this_result = result = 0;
|
this_result = result = 0;
|
||||||
} else if (strcasecmp(attrib, "exec") == 0) {
|
} else if (strcasecmp(attrib, "exec") == 0) {
|
||||||
|
char *conn_hash_hex;
|
||||||
|
|
||||||
if (gethostname(thishost, sizeof(thishost)) == -1)
|
if (gethostname(thishost, sizeof(thishost)) == -1)
|
||||||
fatal("gethostname: %s", strerror(errno));
|
fatal("gethostname: %s", strerror(errno));
|
||||||
strlcpy(shorthost, thishost, sizeof(shorthost));
|
strlcpy(shorthost, thishost, sizeof(shorthost));
|
||||||
|
@ -653,8 +673,11 @@ match_cfg_line(Options *options, char **condition, struct passwd *pw,
|
||||||
snprintf(portstr, sizeof(portstr), "%d", port);
|
snprintf(portstr, sizeof(portstr), "%d", port);
|
||||||
snprintf(uidstr, sizeof(uidstr), "%llu",
|
snprintf(uidstr, sizeof(uidstr), "%llu",
|
||||||
(unsigned long long)pw->pw_uid);
|
(unsigned long long)pw->pw_uid);
|
||||||
|
conn_hash_hex = ssh_connection_hash(thishost, host,
|
||||||
|
portstr, pw->pw_name);
|
||||||
|
|
||||||
cmd = percent_expand(arg,
|
cmd = percent_expand(arg,
|
||||||
|
"C", conn_hash_hex,
|
||||||
"L", shorthost,
|
"L", shorthost,
|
||||||
"d", pw->pw_dir,
|
"d", pw->pw_dir,
|
||||||
"h", host,
|
"h", host,
|
||||||
|
@ -665,6 +688,7 @@ match_cfg_line(Options *options, char **condition, struct passwd *pw,
|
||||||
"u", pw->pw_name,
|
"u", pw->pw_name,
|
||||||
"i", uidstr,
|
"i", uidstr,
|
||||||
(char *)NULL);
|
(char *)NULL);
|
||||||
|
free(conn_hash_hex);
|
||||||
if (result != 1) {
|
if (result != 1) {
|
||||||
/* skip execution if prior predicate failed */
|
/* skip execution if prior predicate failed */
|
||||||
debug3("%.200s line %d: skipped exec "
|
debug3("%.200s line %d: skipped exec "
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* $OpenBSD: readconf.h,v 1.132 2020/01/23 02:46:49 dtucker Exp $ */
|
/* $OpenBSD: readconf.h,v 1.133 2020/04/03 02:27:12 dtucker Exp $ */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Author: Tatu Ylonen <ylo@cs.hut.fi>
|
* Author: Tatu Ylonen <ylo@cs.hut.fi>
|
||||||
|
@ -200,6 +200,8 @@ typedef struct {
|
||||||
#define SSH_STRICT_HOSTKEY_ASK 3
|
#define SSH_STRICT_HOSTKEY_ASK 3
|
||||||
|
|
||||||
const char *kex_default_pk_alg(void);
|
const char *kex_default_pk_alg(void);
|
||||||
|
char *ssh_connection_hash(const char *thishost, const char *host,
|
||||||
|
const char *portstr, const char *user);
|
||||||
void initialize_options(Options *);
|
void initialize_options(Options *);
|
||||||
void fill_default_options(Options *);
|
void fill_default_options(Options *);
|
||||||
void fill_default_options_for_canonicalization(Options *);
|
void fill_default_options_for_canonicalization(Options *);
|
||||||
|
|
120
ssh.c
120
ssh.c
|
@ -1,4 +1,4 @@
|
||||||
/* $OpenBSD: ssh.c,v 1.521 2020/03/06 18:20:02 markus Exp $ */
|
/* $OpenBSD: ssh.c,v 1.522 2020/04/03 02:27:12 dtucker Exp $ */
|
||||||
/*
|
/*
|
||||||
* Author: Tatu Ylonen <ylo@cs.hut.fi>
|
* Author: Tatu Ylonen <ylo@cs.hut.fi>
|
||||||
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
|
* Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
|
||||||
|
@ -86,7 +86,6 @@
|
||||||
#include "canohost.h"
|
#include "canohost.h"
|
||||||
#include "compat.h"
|
#include "compat.h"
|
||||||
#include "cipher.h"
|
#include "cipher.h"
|
||||||
#include "digest.h"
|
|
||||||
#include "packet.h"
|
#include "packet.h"
|
||||||
#include "sshbuf.h"
|
#include "sshbuf.h"
|
||||||
#include "channels.h"
|
#include "channels.h"
|
||||||
|
@ -177,6 +176,13 @@ char *forward_agent_sock_path = NULL;
|
||||||
/* Various strings used to to percent_expand() arguments */
|
/* Various strings used to to percent_expand() arguments */
|
||||||
static char thishost[NI_MAXHOST], shorthost[NI_MAXHOST], portstr[NI_MAXSERV];
|
static char thishost[NI_MAXHOST], shorthost[NI_MAXHOST], portstr[NI_MAXSERV];
|
||||||
static char uidstr[32], *host_arg, *conn_hash_hex;
|
static char uidstr[32], *host_arg, *conn_hash_hex;
|
||||||
|
#define DEFAULT_CLIENT_PERCENT_EXPAND_ARGS \
|
||||||
|
"C", conn_hash_hex, \
|
||||||
|
"L", shorthost, \
|
||||||
|
"i", uidstr, \
|
||||||
|
"l", thishost, \
|
||||||
|
"n", host_arg, \
|
||||||
|
"p", portstr
|
||||||
|
|
||||||
/* socket address the host resolves to */
|
/* socket address the host resolves to */
|
||||||
struct sockaddr_storage hostaddr;
|
struct sockaddr_storage hostaddr;
|
||||||
|
@ -601,8 +607,6 @@ main(int ac, char **av)
|
||||||
extern char *optarg;
|
extern char *optarg;
|
||||||
struct Forward fwd;
|
struct Forward fwd;
|
||||||
struct addrinfo *addrs = NULL;
|
struct addrinfo *addrs = NULL;
|
||||||
struct ssh_digest_ctx *md;
|
|
||||||
u_char conn_hash[SSH_DIGEST_MAX_LENGTH];
|
|
||||||
size_t n, len;
|
size_t n, len;
|
||||||
|
|
||||||
/* Ensure that fds 0, 1 and 2 are open or directed to /dev/null */
|
/* Ensure that fds 0, 1 and 2 are open or directed to /dev/null */
|
||||||
|
@ -1330,15 +1334,8 @@ main(int ac, char **av)
|
||||||
snprintf(uidstr, sizeof(uidstr), "%llu",
|
snprintf(uidstr, sizeof(uidstr), "%llu",
|
||||||
(unsigned long long)pw->pw_uid);
|
(unsigned long long)pw->pw_uid);
|
||||||
|
|
||||||
if ((md = ssh_digest_start(SSH_DIGEST_SHA1)) == NULL ||
|
conn_hash_hex = ssh_connection_hash(thishost, host, portstr,
|
||||||
ssh_digest_update(md, thishost, strlen(thishost)) < 0 ||
|
options.user);
|
||||||
ssh_digest_update(md, host, strlen(host)) < 0 ||
|
|
||||||
ssh_digest_update(md, portstr, strlen(portstr)) < 0 ||
|
|
||||||
ssh_digest_update(md, options.user, strlen(options.user)) < 0 ||
|
|
||||||
ssh_digest_final(md, conn_hash, sizeof(conn_hash)) < 0)
|
|
||||||
fatal("%s: mux digest failed", __func__);
|
|
||||||
ssh_digest_free(md);
|
|
||||||
conn_hash_hex = tohex(conn_hash, ssh_digest_bytes(SSH_DIGEST_SHA1));
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Expand tokens in arguments. NB. LocalCommand is expanded later,
|
* Expand tokens in arguments. NB. LocalCommand is expanded later,
|
||||||
|
@ -1349,14 +1346,9 @@ main(int ac, char **av)
|
||||||
debug3("expanding RemoteCommand: %s", options.remote_command);
|
debug3("expanding RemoteCommand: %s", options.remote_command);
|
||||||
cp = options.remote_command;
|
cp = options.remote_command;
|
||||||
options.remote_command = percent_expand(cp,
|
options.remote_command = percent_expand(cp,
|
||||||
"C", conn_hash_hex,
|
DEFAULT_CLIENT_PERCENT_EXPAND_ARGS,
|
||||||
"L", shorthost,
|
|
||||||
"d", pw->pw_dir,
|
"d", pw->pw_dir,
|
||||||
"h", host,
|
"h", host,
|
||||||
"i", uidstr,
|
|
||||||
"l", thishost,
|
|
||||||
"n", host_arg,
|
|
||||||
"p", portstr,
|
|
||||||
"r", options.user,
|
"r", options.user,
|
||||||
"u", pw->pw_name,
|
"u", pw->pw_name,
|
||||||
(char *)NULL);
|
(char *)NULL);
|
||||||
|
@ -1371,20 +1363,44 @@ main(int ac, char **av)
|
||||||
cp = tilde_expand_filename(options.control_path, getuid());
|
cp = tilde_expand_filename(options.control_path, getuid());
|
||||||
free(options.control_path);
|
free(options.control_path);
|
||||||
options.control_path = percent_expand(cp,
|
options.control_path = percent_expand(cp,
|
||||||
"C", conn_hash_hex,
|
DEFAULT_CLIENT_PERCENT_EXPAND_ARGS,
|
||||||
"L", shorthost,
|
"d", pw->pw_dir,
|
||||||
"h", host,
|
"h", host,
|
||||||
"i", uidstr,
|
|
||||||
"l", thishost,
|
|
||||||
"n", host_arg,
|
|
||||||
"p", portstr,
|
|
||||||
"r", options.user,
|
"r", options.user,
|
||||||
"u", pw->pw_name,
|
"u", pw->pw_name,
|
||||||
"i", uidstr,
|
|
||||||
(char *)NULL);
|
(char *)NULL);
|
||||||
free(cp);
|
free(cp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.identity_agent != NULL) {
|
||||||
|
p = tilde_expand_filename(options.identity_agent, getuid());
|
||||||
|
cp = percent_expand(p,
|
||||||
|
DEFAULT_CLIENT_PERCENT_EXPAND_ARGS,
|
||||||
|
"d", pw->pw_dir,
|
||||||
|
"h", host,
|
||||||
|
"r", options.user,
|
||||||
|
"u", pw->pw_name,
|
||||||
|
(char *)NULL);
|
||||||
|
free(p);
|
||||||
|
free(options.identity_agent);
|
||||||
|
options.identity_agent = cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.forward_agent_sock_path != NULL) {
|
||||||
|
p = tilde_expand_filename(options.forward_agent_sock_path,
|
||||||
|
getuid());
|
||||||
|
cp = percent_expand(p,
|
||||||
|
DEFAULT_CLIENT_PERCENT_EXPAND_ARGS,
|
||||||
|
"d", pw->pw_dir,
|
||||||
|
"h", host,
|
||||||
|
"r", options.user,
|
||||||
|
"u", pw->pw_name,
|
||||||
|
(char *)NULL);
|
||||||
|
free(p);
|
||||||
|
free(options.forward_agent_sock_path);
|
||||||
|
options.forward_agent_sock_path = cp;
|
||||||
|
}
|
||||||
|
|
||||||
if (config_test) {
|
if (config_test) {
|
||||||
dump_client_config(&options, host);
|
dump_client_config(&options, host);
|
||||||
exit(0);
|
exit(0);
|
||||||
|
@ -1509,23 +1525,7 @@ main(int ac, char **av)
|
||||||
if (strcmp(options.identity_agent, "none") == 0) {
|
if (strcmp(options.identity_agent, "none") == 0) {
|
||||||
unsetenv(SSH_AUTHSOCKET_ENV_NAME);
|
unsetenv(SSH_AUTHSOCKET_ENV_NAME);
|
||||||
} else {
|
} else {
|
||||||
p = tilde_expand_filename(options.identity_agent,
|
cp = options.identity_agent;
|
||||||
getuid());
|
|
||||||
cp = percent_expand(p,
|
|
||||||
"d", pw->pw_dir,
|
|
||||||
"h", host,
|
|
||||||
"i", uidstr,
|
|
||||||
"l", thishost,
|
|
||||||
"r", options.user,
|
|
||||||
"u", pw->pw_name,
|
|
||||||
(char *)NULL);
|
|
||||||
free(p);
|
|
||||||
/*
|
|
||||||
* If identity_agent represents an environment variable
|
|
||||||
* then recheck that it is valid (since processing with
|
|
||||||
* percent_expand() may have changed it) and substitute
|
|
||||||
* its value.
|
|
||||||
*/
|
|
||||||
if (cp[0] == '$') {
|
if (cp[0] == '$') {
|
||||||
if (!valid_env_name(cp + 1)) {
|
if (!valid_env_name(cp + 1)) {
|
||||||
fatal("Invalid IdentityAgent "
|
fatal("Invalid IdentityAgent "
|
||||||
|
@ -1539,22 +1539,10 @@ main(int ac, char **av)
|
||||||
/* identity_agent specifies a path directly */
|
/* identity_agent specifies a path directly */
|
||||||
setenv(SSH_AUTHSOCKET_ENV_NAME, cp, 1);
|
setenv(SSH_AUTHSOCKET_ENV_NAME, cp, 1);
|
||||||
}
|
}
|
||||||
free(cp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.forward_agent && (options.forward_agent_sock_path != NULL)) {
|
if (options.forward_agent && options.forward_agent_sock_path != NULL) {
|
||||||
p = tilde_expand_filename(options.forward_agent_sock_path, getuid());
|
|
||||||
cp = percent_expand(p,
|
|
||||||
"d", pw->pw_dir,
|
|
||||||
"h", host,
|
|
||||||
"i", uidstr,
|
|
||||||
"l", thishost,
|
|
||||||
"r", options.user,
|
|
||||||
"u", pw->pw_name,
|
|
||||||
(char *)NULL);
|
|
||||||
free(p);
|
|
||||||
|
|
||||||
if (cp[0] == '$') {
|
if (cp[0] == '$') {
|
||||||
if (!valid_env_name(cp + 1)) {
|
if (!valid_env_name(cp + 1)) {
|
||||||
fatal("Invalid ForwardAgent environment variable name %s", cp);
|
fatal("Invalid ForwardAgent environment variable name %s", cp);
|
||||||
|
@ -1979,14 +1967,9 @@ ssh_session2(struct ssh *ssh, struct passwd *pw)
|
||||||
debug3("expanding LocalCommand: %s", options.local_command);
|
debug3("expanding LocalCommand: %s", options.local_command);
|
||||||
cp = options.local_command;
|
cp = options.local_command;
|
||||||
options.local_command = percent_expand(cp,
|
options.local_command = percent_expand(cp,
|
||||||
"C", conn_hash_hex,
|
DEFAULT_CLIENT_PERCENT_EXPAND_ARGS,
|
||||||
"L", shorthost,
|
|
||||||
"d", pw->pw_dir,
|
"d", pw->pw_dir,
|
||||||
"h", host,
|
"h", host,
|
||||||
"i", uidstr,
|
|
||||||
"l", thishost,
|
|
||||||
"n", host_arg,
|
|
||||||
"p", portstr,
|
|
||||||
"r", options.user,
|
"r", options.user,
|
||||||
"u", pw->pw_name,
|
"u", pw->pw_name,
|
||||||
"T", tun_fwd_ifname == NULL ? "NONE" : tun_fwd_ifname,
|
"T", tun_fwd_ifname == NULL ? "NONE" : tun_fwd_ifname,
|
||||||
|
@ -2143,9 +2126,13 @@ load_public_identity_files(struct passwd *pw)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
cp = tilde_expand_filename(options.identity_files[i], getuid());
|
cp = tilde_expand_filename(options.identity_files[i], getuid());
|
||||||
filename = percent_expand(cp, "d", pw->pw_dir,
|
filename = percent_expand(cp,
|
||||||
"u", pw->pw_name, "l", thishost, "h", host,
|
DEFAULT_CLIENT_PERCENT_EXPAND_ARGS,
|
||||||
"r", options.user, (char *)NULL);
|
"d", pw->pw_dir,
|
||||||
|
"h", host,
|
||||||
|
"r", options.user,
|
||||||
|
"u", pw->pw_name,
|
||||||
|
(char *)NULL);
|
||||||
free(cp);
|
free(cp);
|
||||||
check_load(sshkey_load_public(filename, &public, NULL),
|
check_load(sshkey_load_public(filename, &public, NULL),
|
||||||
filename, "pubkey");
|
filename, "pubkey");
|
||||||
|
@ -2195,10 +2182,9 @@ load_public_identity_files(struct passwd *pw)
|
||||||
cp = tilde_expand_filename(options.certificate_files[i],
|
cp = tilde_expand_filename(options.certificate_files[i],
|
||||||
getuid());
|
getuid());
|
||||||
filename = percent_expand(cp,
|
filename = percent_expand(cp,
|
||||||
|
DEFAULT_CLIENT_PERCENT_EXPAND_ARGS,
|
||||||
"d", pw->pw_dir,
|
"d", pw->pw_dir,
|
||||||
"h", host,
|
"h", host,
|
||||||
"i", uidstr,
|
|
||||||
"l", thishost,
|
|
||||||
"r", options.user,
|
"r", options.user,
|
||||||
"u", pw->pw_name,
|
"u", pw->pw_name,
|
||||||
(char *)NULL);
|
(char *)NULL);
|
||||||
|
|
30
ssh_config.5
30
ssh_config.5
|
@ -33,8 +33,8 @@
|
||||||
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||||
.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
.\"
|
.\"
|
||||||
.\" $OpenBSD: ssh_config.5,v 1.322 2020/02/07 03:54:44 dtucker Exp $
|
.\" $OpenBSD: ssh_config.5,v 1.323 2020/04/03 02:27:12 dtucker Exp $
|
||||||
.Dd $Mdocdate: February 7 2020 $
|
.Dd $Mdocdate: April 3 2020 $
|
||||||
.Dt SSH_CONFIG 5
|
.Dt SSH_CONFIG 5
|
||||||
.Os
|
.Os
|
||||||
.Sh NAME
|
.Sh NAME
|
||||||
|
@ -1845,31 +1845,23 @@ otherwise.
|
||||||
The local username.
|
The local username.
|
||||||
.El
|
.El
|
||||||
.Pp
|
.Pp
|
||||||
.Cm Match exec
|
.Cm Match exec ,
|
||||||
accepts the tokens %%, %h, %i, %L, %l, %n, %p, %r, and %u.
|
.Cm CertificateFile ,
|
||||||
.Pp
|
.Cm ControlPath ,
|
||||||
.Cm CertificateFile
|
.Cm IdentityAgent ,
|
||||||
accepts the tokens %%, %d, %h, %i, %l, %r, and %u.
|
.Cm IdentityFile ,
|
||||||
.Pp
|
and
|
||||||
.Cm ControlPath
|
.Cm RemoteCommand
|
||||||
accepts the tokens %%, %C, %h, %i, %L, %l, %n, %p, %r, and %u.
|
accept the tokens %%, %C, %d, %h, %i, %L, %l, %n, %p, %r, and %u.
|
||||||
.Pp
|
.Pp
|
||||||
.Cm Hostname
|
.Cm Hostname
|
||||||
accepts the tokens %% and %h.
|
accepts the tokens %% and %h.
|
||||||
.Pp
|
.Pp
|
||||||
.Cm IdentityAgent
|
|
||||||
and
|
|
||||||
.Cm IdentityFile
|
|
||||||
accept the tokens %%, %d, %h, %i, %l, %r, and %u.
|
|
||||||
.Pp
|
|
||||||
.Cm LocalCommand
|
.Cm LocalCommand
|
||||||
accepts the tokens %%, %C, %d, %h, %i, %l, %n, %p, %r, %T, and %u.
|
accepts all tokens.
|
||||||
.Pp
|
.Pp
|
||||||
.Cm ProxyCommand
|
.Cm ProxyCommand
|
||||||
accepts the tokens %%, %h, %n, %p, and %r.
|
accepts the tokens %%, %h, %n, %p, and %r.
|
||||||
.Pp
|
|
||||||
.Cm RemoteCommand
|
|
||||||
accepts the tokens %%, %C, %d, %h, %i, %l, %n, %p, %r, and %u.
|
|
||||||
.Sh FILES
|
.Sh FILES
|
||||||
.Bl -tag -width Ds
|
.Bl -tag -width Ds
|
||||||
.It Pa ~/.ssh/config
|
.It Pa ~/.ssh/config
|
||||||
|
|
Loading…
Reference in New Issue