Bash hostname completion

As part of its programmable completion suite, Bash includes hostname completion. This completion mode reads hostnames from a file in hosts(5) format to find possible completions matching the current word. On Unix-like operating systems, it defaults to reading the file in its usual path at /etc/hosts.

For example, given the following hosts(5) file in place at /etc/hosts:

127.0.0.1      localhost
192.0.2.1      web.example.com www
198.51.100.10  mail.example.com mx
203.0.113.52   radius.example.com rad

An appropriate call to compgen would yield this output:

$ compgen -A hostname
localhost
web.example.com
www
mail.example.com
mx
radius.example.com
rad

We could then use this to complete hostnames for network diagnostic tools like ping(8):

$ complete -A hostname ping

Typing ping we and then pressing Tab would then complete to ping web.example.com. If the shopt option hostcomplete is on, which it is by default, Bash will also attempt host completion if completing any word with an @ character in it. This can be useful for email address completion or for SSH username@hostname completion.

We could also trigger hostname completion in any other Bash command line (regardless of complete settings) with the Readline shortcut Alt+@ (i.e. Alt+Shift+2). This works even if hostcomplete is turned off.

However, with DNS so widely deployed, and with system /etc/hosts files normally so brief on internet-connected systems, this may not seem terribly useful; you’d just end up completing localhost, and (somewhat erroneously) a few IPv6 addresses that don’t begin with a digit. It may seem even less useful if you have your own set of hosts in which you’re interested, since they may not correspond to the hosts in the system’s /etc/hosts file, and you probably really do want them looked up via DNS each time, rather than maintaining static addresses for them.

There’s a simple way to make host completion much more useful by defining the HOSTFILE variable in ~/.bashrc to point to any other file containing a list of hostnames. You could, for example, create a simple file ~/.hosts in your home directory, and then include this in your ~/.bashrc:

# Use a private mock hosts(5) file for completion
HOSTFILE=$HOME/.hosts

You could then populate the ~/.hosts file with a list of hostnames in which you’re interested, which will allow you to influence hostname completion usefully without messing with your system’s DNS resolution process at all. Because of the way the Bash HOSTFILE parsing works, you don’t even have to fake an IP address as the first field; it simply scans the file for any word that doesn’t start with a digit:

# Comments with leading hashes will be excluded
external.example.com
router.example.com router
github.com
google.com
...

You can even include other files from it with an $include directive!

$include /home/tom/.hosts.home
$include /home/tom/.hosts.work

This really surprised me when reading the source, because I don’t think /etc/hosts files generally support that for their usual name resolution function. I would love to know if any systems out there actually do support this.

The behaviour of the HOSTFILE variable is a bit weird; all of the hosts from the HOSTFILE are appended to the in-memory list of completion hosts each time the HOSTFILE variable is set (not even just changed), and host completion is attempted, even if the hostnames were already in the list. It’s probably sufficient just to set the file once in ~/.bashrc.

This setup allows you to set hostname completion as the default method for all sorts of network-poking tools, falling back on the usual filename completion if nothing matches with -o default:

$ complete -A hostname -o default curl dig host netcat ping telnet

You could also use hostname completions for ssh(1), but to account for hostname aliases and other ssh_config(5) tricks, I prefer to read Host directives values from ~/.ssh/config for that.

If you have machine-readable access to the complete zone data for your home or work domain, it may even be worth periodically enumerating all of the hostnames into that file, perhaps using rndc dumpdb -zones for a BIND9 setup, or using an AXFR request. If you have a locally caching recursive nameserver, you could even periodically examine the contents of its cache for new and interesting hosts to add to the file.

Testing HTTP/1.1 responses

Before changing DNS A records for a website, it’s prudent to check that the webserver with the IP address to which you’re going to change the records will actually serve a website with the relevant hostname; that is, if it’s an Apache HTTPD webserver, that it has a valid VirtualHost definition for the site.

If you don’t actually have administrative access to the webserver to check this, there are many basic ways to test it; from the command line, three of the most useful include using curl, wget, or plain old telnet. For each, the method comprises manipulating the HTTP/1.1 request of the target webserver such that the website you want to test is used as the hostname in the Host header.

Using curl

Perhaps the quickest and tidiest way to check this from a Unix command line is using curl, the binary frontend to the libcurl library. You do this by making an HTTP/1.1 request of the target server’s IP address, while including an explicitly specified value for the Host. This is done using the -H option:

$ curl -H "Host: sanctum.geek.nz" 120.138.30.239

This spits out quite a lot of information, including some on stderr, so you may choose to filter it and just check for the <title> tag, with a little bit of context, to make sure the site you expected really is being returned as the appropriate response:

$ !! 2>/dev/null | grep -C3 '<title>'
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Arabesque | Systems, Tools, and Terminal Science</title>
<link rel="profile" href="http://gmpg.org/xfn/11" />
<link rel="stylesheet" type="text/css" media="all" href="https://sanctum...
<link rel="pingback" href="https://blog.sanctum.geek.nz/xmlrpc.php" />

Using wget

An equivalent to the curl method can be achieved using the --header option for the commonly available wget:

$ wget --header="Host: sanctum.geek.nz/arabesque" 69.163.229.57 -q -O -

Using telnet

If you don’t have curl available, Telnet works just as well on both Windows and Unix-like systems, though it’s a little more awkward to work with, as you have to type the request and its headers straight into the TCP session:

$ telnet 69.163.229.57 80
Trying 69.163.229.57...
Connected to 69.163.229.57.
Escape character is '^]'.
GET / HTTP/1.1
Host: sanctum.geek.nz/arabesque

Note that you need to press Enter twice after writing in the hostname to check to complete the HTTP request. If this spits the HTML of your expected page back at you and closes the connection, then you’ve got some indication that things are configured correctly.

Yet another option to test this, particularly if you want to actually view the site in a browser, is to change your system’s hosts file to force DNS resolution to be different for the appropriate hostname on your local system.

Thanks to commenter Jaime Herazo for suggesting the wget method in the comments, and commenter sam for suggesting the -C option for grep.

Restricting public keys

It may be the case that while you’re happy to allow a user or process to have public key authentication access to your server via the ~/.ssh/authorized_keys file, you don’t necessarily want to give them a full shell, or you may want to restrict them from doing things like SSH port forwarding or X11 forwarding.

One method that’s supposed to prevent users from accessing a shell is by defining their shell in /etc/passwd as /bin/false, which does indeed prevent them from logging in with the usual ssh or ssh command syntax. This isn’t a good approach because it still allows port forwarding and other SSH-enabled services.

If you want to restrict the use of logins with a public key, you can prepend option pairs to its line in the authorized_keys file. Some of the most useful options here include:

  • from="<hostname/ip>" — Prepending from="*.example.com" to the key line would only allow public-key authenticated login if the connection was coming from some host with a reverse DNS of example.com. You can also put IP addresses in here. This is particularly useful for setting up automated processes through keys with null passphrases.
  • command="<command>" — Means that once authenticated, the command specified is run, and the connection is closed. Again, this is useful in automated setups for running only a certain script on successful authentication, and nothing else.
  • no-agent-forwarding — Prevents the key user from forwarding authentication requests to an SSH agent on their client, using the -A or ForwardAgent option to ssh.
  • no-port-forwarding — Prevents the key user from forwarding ports using -L and -R.
  • no-X11-forwarding — Prevents the key user from forwarding X11 processes.
  • no-pty — Prevents the key user from being allocated a tty device at all.

So, for example, a public key that is only used to run a script called runscript on the server by the client runscript@client.example:

command="runscript",client="client.example",no-pty,no-agent-forwarding,no-port-forwarding ssh-rsa AAAAB2....19Q runscript@client.example

A public key for a user whom you were happy to allow to log in from anywhere with a full shell, but did not want to allow agent, port, or X11 forwarding:

no-agent-forwarding,no-port-forwarding,no-X11-forwarding ssh-rsa AAAAD3....19Q user@client.example

Use of these options goes a long way to making your public key authentication setup harder to exploit, and is very consistent with the principle of least privilege. To see a complete list of the options available, check out the man page for sshd.

Automatic tmux titles

If you’re using tmux as a terminal multiplexer and keeping one window open per host, you might be manually renaming each window to feature the relevant hostname. This is a little annoying to do if you’re dealing with a lot of hosts, so it’s worthwhile to automate it.

In the tmux manual, the following escape code incantation is given to update the window title from within the terminal:

$ printf '\033kWINDOW_NAME\033\\'

In much the same way that you can update the title of an xterm-compatible terminal emulator with control codes as part of the $PS1 variable defining the prompt, you can update the title of a tmux window to the current hostname (or any other relevant text) automatically by prefixing this call to the $PROMPT_COMMAND. This is best done in your .bashrc. The below code assumes you are using either screen or screen-256color as your $TERM string in your .tmux.conf:

case "$TERM" in
    screen*)
        PROMPT_COMMAND="printf '\033k$(hostname)\033\\';"${PROMPT_COMMAND}
        ;;
esac

After logging out and in again, this will update the title of the window to the hostname of the current machine just before the prompt is presented, saving you the trouble of updating the window title if like myself you never use it for anything besides the machine’s hostname.