add asiabsdcon paper
This commit is contained in:
parent
8859a8a517
commit
baab2e76c0
6347
asiabsdcon2023/IEEEtran.cls
Normal file
6347
asiabsdcon2023/IEEEtran.cls
Normal file
File diff suppressed because it is too large
Load Diff
BIN
asiabsdcon2023/asiabsdcon_2023_final.pdf
Normal file
BIN
asiabsdcon2023/asiabsdcon_2023_final.pdf
Normal file
Binary file not shown.
686
asiabsdcon2023/asiabsdcon_2023_final.tex
Normal file
686
asiabsdcon2023/asiabsdcon_2023_final.tex
Normal file
@ -0,0 +1,686 @@
|
||||
% Intended LaTeX compiler: pdflatex
|
||||
\documentclass[conference]{IEEEtran}
|
||||
%\documentclass[11pt]{article}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{longtable}
|
||||
\usepackage{wrapfig}
|
||||
\usepackage{rotating}
|
||||
\usepackage[normalem]{ulem}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
%\usepackage{capt-of}
|
||||
\usepackage{hyperref}
|
||||
\date{2023-03-07}
|
||||
\title{Dynamic host configuration, please}
|
||||
\hypersetup{
|
||||
pdfauthor={Florian Obser},
|
||||
pdftitle={Dynamic host configuration, please},
|
||||
pdfkeywords={},
|
||||
pdfsubject={},
|
||||
pdfcreator={Emacs 28.2 (Org mode 9.5.5)},
|
||||
pdflang={English}}
|
||||
\author{\IEEEauthorblockN{Florian Obser}
|
||||
\IEEEauthorblockA{florian@openbsd.org}
|
||||
}
|
||||
\begin{document}
|
||||
|
||||
\maketitle
|
||||
\begin{abstract}
|
||||
\label{sec:orgdbc4e3b}
|
||||
Smartphones are always online devices in urban areas. They are even
|
||||
mostly online in rural areas. They deal with many different kinds of
|
||||
networks with only minimal configuration from the user. This paper
|
||||
will cover how we achieved a similar user experience on OpenBSD
|
||||
laptops. We will cover how we remember past visited Wi-Fi networks,
|
||||
automatically configuring IPv4 and IPv6 addresses and dealing with DNS
|
||||
in challenging network environments. We will also point out security
|
||||
measurements we put in place while dealing with untrusted networks.
|
||||
\end{abstract}
|
||||
|
||||
\section{Join the Wi-Fi}
|
||||
\label{sec:orgff06dc2}
|
||||
When we bring a device to a never-before-visited network location we
|
||||
need the network name and password for the Wi-Fi or select an open
|
||||
Wi-Fi network.
|
||||
|
||||
Smartphones provide a user interface (UI) for this where we select
|
||||
the Wi-Fi from a drop-down list and then we are prompted for a
|
||||
password.
|
||||
|
||||
On OpenBSD, network interfaces are configured by \href{https://man.openbsd.org/ifconfig.8}{ifconfig(8)}, or
|
||||
persistently in \href{https://man.openbsd.org/hostname.if.5}{/etc/hostname.IF}\footnote{IF denotes a specific network
|
||||
interface. For example for iwm0 the file is \texttt{/etc/hostname.iwm0}},
|
||||
which is read by \href{https://man.openbsd.org/netstart.8}{netstart(8)} during boot. netstart(8) calls ifconfig(8)
|
||||
internally to handle the network configuration. \texttt{\# ifconfig iwm0 scan}
|
||||
will list the available Wi-Fi networks for the \texttt{iwm0} interface.
|
||||
|
||||
For a long time, we could only configure one Wi-Fi network:
|
||||
\begin{verbatim}
|
||||
$ cat /etc/hostname.iwm0
|
||||
nwid home wpakey "trivial password"
|
||||
inet autoconf
|
||||
inet6 autoconf
|
||||
up
|
||||
\end{verbatim}
|
||||
|
||||
This configures a Wi-Fi network named "home" and a password "trivial
|
||||
password". IPv4 and IPv6 auto-configuration are enabled. Whenever the
|
||||
network is in range the kernel automatically connects to it.
|
||||
|
||||
That is not a good user experience (UX). We typically take our laptops
|
||||
with us and connect to different Wi-Fi networks, like our smartphones. We
|
||||
have a Wi-Fi at home, at work, there are open Wi-Fis at hotels, and so
|
||||
on.
|
||||
|
||||
People came up with all kinds of unusual shell scripts that would run in
|
||||
the background or were triggered by \href{https://man.openbsd.org/cron.8}{cron(8)} to notice when the laptop moved
|
||||
to a different Wi-Fi. The script would then call ifconfig(8) to
|
||||
reconfigure Wi-Fi from a list of networks it knew about. This was all fragile and unlike how OpenBSD works.
|
||||
|
||||
Peter Hessler (phessler@), with the help of Stefan Sperling (stsp@)
|
||||
went ahead and tackled this problem: what if we could pass multiple
|
||||
\texttt{(name, password)} tuples to the kernel and the kernel would choose the
|
||||
right one?
|
||||
|
||||
\begin{verbatim}
|
||||
$ cat /etc/hostname.iwm0
|
||||
join home wpakey "trivial password"
|
||||
join work wpakey zUDciIezevfySqam
|
||||
join "Airport Wi-Fi"
|
||||
join ""
|
||||
inet autoconf
|
||||
inet6 autoconf
|
||||
up
|
||||
\end{verbatim}
|
||||
\texttt{join} implements exactly this. The argument to \texttt{join} is the name of
|
||||
the network and the following \texttt{wpakey} is the password for that
|
||||
network. If we leave out the \texttt{wpakey}, the Wi-Fi is open and does not
|
||||
require a password. Using \texttt{join} with the empty string (\texttt{join ""})
|
||||
means the kernel will try to connect to any open Wi-Fi if no Wi-Fi
|
||||
from the join-list is found first.
|
||||
|
||||
We still need to configure the name and password by editing a file
|
||||
in \texttt{/etc/} and run netstart(8) when we encounter a new Wi-Fi. This is
|
||||
probably not the best UI\footnote{ed(1) is the
|
||||
pinnacle of UI design, as far as the author is concerned.} but the UX is pretty good and on par
|
||||
with a smartphone. Once the Wi-Fi has been configured by adding a \texttt{join}
|
||||
line, the kernel will automatically reconnect to a known Wi-Fi
|
||||
whenever it comes within range.
|
||||
\section{Stop slacking}
|
||||
\label{sec:orgcc75305}
|
||||
Now that we are connected to the Wi-Fi, we need to configure IP
|
||||
addresses.
|
||||
|
||||
We started our efforts to improve the network configuration user
|
||||
experience with IPv6 for two reasons. One is that IPv6 is still a technology for early adopters
|
||||
who are used to difficulties when using new technologies and are eager to help debug the problems that might arise.
|
||||
|
||||
Another reason for our work was the fact that OpenBSD got IPv6 support from the KAME project
|
||||
in the late 1990s and early 2000s after which it quieted down again.
|
||||
The network configuration was handled mostly in the
|
||||
kernel, so there was no isolation from malicious input. For the most
|
||||
part it assumed a stationary workstation that tried to acquire an
|
||||
IPv6 prefix for stateless address auto-configuration during boot by
|
||||
sending three router solicitations and then listened for router
|
||||
advertisements to create auto-configuration addresses and renewed
|
||||
their lifetimes when a new advertisement flew by. There was some
|
||||
rudimentary code in rtsold(8) to handle movement between networks, but
|
||||
it was rarely used since it was optional. rtsold(8) was used in
|
||||
one-shot mode where it would send at most three router solicitations
|
||||
when an interface connected to the network and then it would exit.
|
||||
|
||||
We started to write \href{https://man.openbsd.org/slaacd.8}{slaacd(8)}
|
||||
and once that was working we could delete rtsold(8)
|
||||
and remove considerable pieces of code from the kernel.
|
||||
|
||||
slaacd(8) is a privilege-separated network daemon that build previous
|
||||
experience with privilege separation in OpenBSD. It uses three
|
||||
processes, (1) the \emph{parent} process to configure the system, (2) the
|
||||
\emph{frontend} process to talk to the outside world and (3) the \emph{engine}
|
||||
process to handle untrusted data and run a state machine for the
|
||||
stateless address auto-configuration protocol.
|
||||
|
||||
pledge(2) restricts what a process is allowed to do and this is
|
||||
enforced by the kernel. Enforcement means that the kernel will
|
||||
terminate processes that violate what they have pledged to do. The
|
||||
pledges themselves are in broad strokes, we do not concern ourselves
|
||||
with single system calls but with groups of system calls. For example,
|
||||
the process is allowed to interact with open file descriptors
|
||||
(\texttt{"stdio"}), it is allowed to open connections to hosts on the
|
||||
Internet (\texttt{"inet"}), and it is allowed to open files for reading
|
||||
(\texttt{"rpath"}).
|
||||
|
||||
The \emph{parent} process pledges that it will only open new network
|
||||
sockets, send those to other processes and reconfigure the routing
|
||||
table (\texttt{"stdio inet sendfd wroute"}). The \emph{frontend} process pledges
|
||||
to only receive file descriptors, open unix domain sockets and check
|
||||
the state of the routing table (\texttt{"stdio unix recvfd route"}). Checking
|
||||
the routing table includes seeing which flags are configured per
|
||||
interface. The \emph{engine} process pledges to only read and write to
|
||||
already open file-descriptors (\texttt{"stdio"}). The \emph{engine} process is
|
||||
very restricted in what it is allowed to do. This is important because it
|
||||
handles untrusted data coming from the network. While the \emph{frontend}
|
||||
process talks to the network, it never looks at the data. An attacker
|
||||
will not be able to confuse the \emph{frontend} process with data they
|
||||
send. They can and have \href{https://ftp.openbsd.org/pub/OpenBSD/patches/7.0/common/014\_slaacd.patch.sig}{confused} the \emph{engine} process.
|
||||
|
||||
For more details see \href{privsep.org}{"Privilege drop, privilege separation, and
|
||||
restricted-service operating mode in OpenBSD"}.
|
||||
|
||||
slaacd(8) is enabled per default on all OpenBSD installations.
|
||||
|
||||
IPv6 stateless address auto-configuration is enabled on an interface
|
||||
by setting the \texttt{AUTCONF6} flag using
|
||||
\href{https:///man.openbsd.org/ifconfig.8}{ifconfig(8)}: \texttt{ifconfig iwm0 inet6
|
||||
autoconf}. The kernel announces this changed interface flag to the
|
||||
whole system using a broadcast route message. slaacd(8) reads those
|
||||
messages using a \href{https://man.openbsd.org/route.4}{route(4)} socket.
|
||||
|
||||
slaacd(8) handles all aspects of stateless address
|
||||
auto-configuration. It sends router solicitations when needed, either
|
||||
multi-cast or uni-cast, depending on which is appropriate. It waits
|
||||
for router advertisements, parses them, and configures default routes,
|
||||
global and temporary IPv6 addresses, and passes name server
|
||||
information via a route message to the rest of the system. It takes
|
||||
care of the lifetimes of addresses, default routes, and name server
|
||||
information expiring and removes those from the system when no router
|
||||
advertisements are received to extend the lifetime.
|
||||
|
||||
slaacd(8) also monitors when network interfaces regain their
|
||||
connection to a network. For example because the laptop woke up from
|
||||
suspension or it got moved out of range of a Wi-Fi network and moved back
|
||||
within range. It then needs to find out whether it connected to the same
|
||||
network as before or whether it is now in a new network. If it is a new
|
||||
network we need to replace the old addresses, default route and name
|
||||
servers. If there is no IPv6 available it needs to remove the old
|
||||
information.
|
||||
|
||||
The stateless address auto-configuration specification allows multiple
|
||||
default routers being present on the same layer-two network,
|
||||
announcing the same or different network information. slaacd(8) tries
|
||||
to handle this, but this has not been extensively tested in all
|
||||
possible cases. There are still open questions being discussed at the
|
||||
IETF on how to run networks with different network prefixes in the
|
||||
same layer-two network.
|
||||
|
||||
slaacd(8) is able to handle multiple interfaces and we will show
|
||||
later how we pick the right source address when multiple addresses are available
|
||||
to choose from.
|
||||
|
||||
\section{Dynamic host configuration, please}
|
||||
\label{sec:org76b66c3}
|
||||
With IPv6 address configuration mostly solved, it was time to look at
|
||||
IPv4 again. We used a fork of ISC's dhclient(8). Henning Brauer
|
||||
(henning@) added privilege-separation to it and Kenneth Westerback (krw@)
|
||||
has been maintaining it in the past few years. However, the privilege-separation was never quite right which
|
||||
became more visible with the integration of pledge(2) and it turned out to be
|
||||
difficult to integrate some of the features we developed in slaacd(8).
|
||||
|
||||
It was time to write a new daemon and Otto Moerbeek (otto@) came up with a name for it: dhcpleased(8).
|
||||
It is pronounced as "dynamic host configuration, please" with the "d" silent.
|
||||
|
||||
On a very high level, IPv4 DHCP and IPv6 stateless address
|
||||
auto-configuration are very similar. We request some information from
|
||||
the router\footnote{In IPv6 we might not need to request the information,
|
||||
it might just show up unannounced.}, we use it to configure the system
|
||||
and we make sure that information does not expire. When we move
|
||||
networks we need to probe whether our information is still up to date and
|
||||
if not, reconfigure the system.
|
||||
|
||||
We opted for the obvious solution, which is to copy \texttt{sbin/slaacd} to \texttt{sbin/dhcpleased} and
|
||||
replace the IPv6 specific bits with IPv4 specific bits.
|
||||
|
||||
On paper DHCP looks more complicated than IPv6 stateless address
|
||||
auto-configuration because it negotiates with the server and there is
|
||||
a complicated state machine to implement.
|
||||
|
||||
In practice it is the other way around. The "stateless" part in IPv6
|
||||
does not apply to the client. The client must keep state and implement
|
||||
a state machine to keep track of which routers are available and when
|
||||
various information expires. In IPv4 we talk to one server and all
|
||||
information expires at the same time.
|
||||
|
||||
We will talk about a few differences between slaacd(8) and
|
||||
dhcpleased(8) in a moment, but from the user perspective both behave
|
||||
in the same way. They make sure that the address configuration and default
|
||||
gateway are always up to date and they pay attention when the machine
|
||||
moves between networks, either while awake or while sleeping.
|
||||
|
||||
Because dhcpleased(8) has to use \href{https://man.openbsd.org/bpf.4}{bpf(4)} instead of regular sockets for
|
||||
some of the network packets it needs to send, the \emph{parent} process
|
||||
cannot use pledge(2). Currently, there is nothing it could pledge that would
|
||||
allow the usage of bpf(4). To protect the system and
|
||||
prevent exfiltration of sensitive data we use \href{https://man.openbsd.org/unveil.2}{unveil(2)} to restrict
|
||||
the \emph{parent} process' view of the file system. dhcpleased(8) can only
|
||||
read its configuration file, read and write \texttt{/dev/bpf}, and read,
|
||||
write and create files underneath \texttt{/var/db/dhcpleased/} to store
|
||||
information about received leases.
|
||||
|
||||
While we could get away with not implementing a config file for
|
||||
slaacd(8), this did not work for dhcpleased(8). Some systems out
|
||||
there will only give us a DHCP lease if we send the correct \emph{client
|
||||
id}, for example.
|
||||
|
||||
There are many DHCP options specified in RFC 2132. We have only
|
||||
implemented the bare minimum, only the options we need and can
|
||||
handle. We do not need a swap server or a cookie server, to name a few.
|
||||
|
||||
Like slaacd(8), dhcpleased(8) is enabled on all OpenBSD
|
||||
installations.
|
||||
\section{Route priorities}
|
||||
\label{sec:orgefb99be}
|
||||
dhcpleased(8) and slaacd(8) can handle multiple interfaces at the same
|
||||
time. The routing table might look like this:
|
||||
|
||||
\begin{verbatim}
|
||||
$ netstat -nrf inet \
|
||||
| awk "{print $1,$2,$7,$8}"
|
||||
Routing tables
|
||||
|
||||
Internet:
|
||||
Destination Gateway Prio Iface
|
||||
default 192.168.1.1 8 em0
|
||||
default 192.168.178.1 12 iwm0
|
||||
[...]
|
||||
\end{verbatim}
|
||||
|
||||
We end up with two default routes, one gateway is reachable via the
|
||||
\emph{em0} interface with priority value 8 and the other gateway is
|
||||
reachable via the \emph{iwm0} interface with priority value 12. A route has
|
||||
higher priority when its priority value is lower. \emph{em0} is an Ethernet
|
||||
interface and it gets higher priority over the Wi-Fi interface
|
||||
\emph{iwm0}. All things being equal, the kernel will pick the address from
|
||||
\emph{em0} as source address when making a new connection to the internet
|
||||
and route traffic over the Ethernet interface, which is presumably
|
||||
faster.
|
||||
|
||||
If we pick up the laptop and unplug the Ethernet interface, the route over \emph{em0} is no longer usable and
|
||||
existing connections using it will stall and time out. New connections
|
||||
will instead use \emph{iwm0}.
|
||||
|
||||
If we plug the Ethernet interface \emph{em0} back in, the session might come alive again and new
|
||||
connections will use \emph{em0}. Connections that are running over \emph{iwm0}
|
||||
will continue working, because the interface is still connected to
|
||||
the Wi-Fi.
|
||||
|
||||
Applications like web browsers, email clients or even video
|
||||
conferencing systems will automatically establish a new connection
|
||||
when they notice that the old one is dead.
|
||||
|
||||
Unfortunately \href{https://man.openbsd.org/ssh.1}{ssh(1)} is not one of them. If switching between wired
|
||||
and wireless happens rarely, \href{https://man.openbsd.org/tmux.1}{tmux(1)} on the remote system might help
|
||||
with ssh(1) disconnects, or a \href{https://man.openbsd.org/wg.4}{wg(4)} tunnel can be used so that
|
||||
the source address does not change when switching between wired and
|
||||
wireless.
|
||||
\section{Cellular networks}
|
||||
\label{sec:org7d7219f}
|
||||
In addition to Ethernet and Wi-Fi networks, OpenBSD supports "Mobile
|
||||
Broadband Interface Model" devices using the \href{https://man.openbsd.org/umb.4}{umb(4)} driver. These can
|
||||
be used to connect to UMTS or LTE networks. They require a SIM card
|
||||
and after being configured using a PIN they will connect to cellular
|
||||
networks and automatically configure an IP address and default
|
||||
route. The default route has an even lower route priority than Wi-Fi
|
||||
so it will only be used when Ethernet and Wi-Fi are not connected.
|
||||
\section{It is always DNS}
|
||||
\label{sec:orgd2b8f30}
|
||||
Humans are not particularly good at remembering
|
||||
addresses like \texttt{2606:2800:220:1:248:1893:25c8:1946} and are much better
|
||||
with names like \emph{example.com}. When we run \texttt{ping6 example.com} we
|
||||
will end up in \href{https://man.openbsd.org/asr\_run.3}{libc's stub resolver}. It will open
|
||||
\texttt{/etc/resolv.conf} and look for \emph{nameserver} lines to use for DNS
|
||||
resolution.
|
||||
|
||||
We can learn name servers from dhcpleased(8), slaacd(8), umb(4),
|
||||
and \href{https://man.openbsd.org/iked.8}{iked(8)}. Historically dhclient(8) owned \texttt{/etc/resolv.conf}, which
|
||||
means that no other process could add name servers to it. dhclient(8)
|
||||
would just overwrite whatever was in there whenever it renewed its
|
||||
lease. This made it impossible to sometimes move to an IPv6-only
|
||||
network. slaacd(8) could not configure name servers and the left-over
|
||||
IPv4 name servers were not reachable.
|
||||
|
||||
We can either teach all name server sources to somehow cooperate and share responsibility of
|
||||
\texttt{/etc/resolv.conf} or we can run an arbitrator that collects name
|
||||
servers from diverse sources and handles the contents of
|
||||
\texttt{/etc/resolv.conf}.
|
||||
|
||||
\href{https://man.openbsd.org/resolvd.8}{resolvd(8)} is such an arbitrator. It is another always enabled
|
||||
daemon. It collects name servers from all the mentioned sources and
|
||||
adds them to \texttt{/etc/resolv.conf}.
|
||||
|
||||
It also monitors if \texttt{/etc/resolv.conf} gets edited in which case it
|
||||
rereads the file and makes sure that the learned name servers are at
|
||||
the beginning of the file. This is useful when the administrator of
|
||||
the machine decides to add options to \texttt{/etc/resolv.conf}. For example,
|
||||
we can edit the file and add \texttt{family inet6 inet} to prefer IPv6 over
|
||||
IPv4 and resolvd(8) will cope. There is no need for an extra
|
||||
configuration file, \texttt{/etc/resolv.conf} is the configuration file.
|
||||
|
||||
Name servers are announced using route messages and resolvd(8) listens
|
||||
for them using a route(4) socket. They can also be observed using the
|
||||
\href{https://man.openbsd.org/route.8}{route(8)} tool: \texttt{\$ route monitor}.
|
||||
|
||||
resolvd(8) can also request that name servers are re-announced by their
|
||||
sources. This is useful when resolvd(8) gets restarted.
|
||||
\section{Let us unwind a bit}
|
||||
\label{sec:orgab25916}
|
||||
Plain DNS is not a secure protocol. It exchanges
|
||||
unauthenticated UDP packets without any integrity protection. This
|
||||
makes it easy for an attacker to spoof answer packets.
|
||||
|
||||
DNS answer packets are untrusted data, they come from the
|
||||
network. However, the process that sends DNS queries and parses the
|
||||
answer using the libc functions is almost always the single main
|
||||
process of the tool. When we run \texttt{ping example.com}, DNS packets are
|
||||
parsed using our user. An attacker who can spoof a DNS answer might be
|
||||
able to trigger a bug in libc and gain code execution that way.
|
||||
|
||||
On OpenBSD, ping(8) pledges \texttt{"stdio DNS"} so the attacker will not get
|
||||
very far, but there are many more programs in ports that are not
|
||||
pledged that might want to resolve names.
|
||||
|
||||
It would be worthwhile to have some sort of proxy running on localhost
|
||||
such that DNS packets from the outside need to traverse a well locked
|
||||
down process running in a different address-space and as a different
|
||||
user than the program that needs to resolve a name.
|
||||
|
||||
An early experiment was rebound(8), written by Ted Unangst (tedu@). It
|
||||
was simplistic and did not understand DNS at all, it would just
|
||||
forward packets, but it would sit between the Internet and the
|
||||
program.
|
||||
|
||||
An alternative is to run a full recursive resolver like \href{https://man.openbsd.org/unbound.8}{unbound(8)} on
|
||||
the laptop, but this leads to problems, too. unbound(8) expects a well
|
||||
working network where nothing interferes with DNS, this is true in data
|
||||
centres and can be achieved in well maintained home networks, but it
|
||||
is not something we find when moving laptops to arbitrary networks
|
||||
like free Wi-Fi in a hotel or at an airport.
|
||||
|
||||
It turns out that often the quality of the network changes over
|
||||
time. When we first connect to a hotel Wi-Fi we may find ourselves in
|
||||
what is referred to as a \emph{captive portal}. Everything is blocked, DNS
|
||||
gets intercepted, and we are redirected to a website where we need to
|
||||
agree to the terms and conditions and maybe provide our name and room
|
||||
number. Once we are past that, network quality improves considerably
|
||||
and we are mostly free to talk to the outside world.
|
||||
|
||||
This is where \href{https://man.openbsd.org/unwind.8}{unwind(8)} comes in.
|
||||
It is another privilege-separated
|
||||
network daemon that provides a recursive name server for the local
|
||||
machine. resolvd(8) detects when it is running and automatically
|
||||
rewrites \texttt{/etc/resolv.conf} to have only \texttt{nameserver 127.0.0.1} listed
|
||||
as name server.
|
||||
|
||||
This solves or improves upon the first problem. Programs that need DNS resolution are insulated from
|
||||
the Internet. An attacker needs to get past unwind(8) first before
|
||||
they can try to attack the libc stub resolver.
|
||||
|
||||
unwind(8) understands and speaks DNS and it actively observes the
|
||||
network quality.
|
||||
|
||||
We did not write our own recursive name server. That would be
|
||||
difficult and since DNS is constantly evolving, it would also require extra work to keep up. Instead we
|
||||
decided to use libunbound, which is
|
||||
part of \href{https://man.openbsd.org/unbound.8}{unbound(8)}. It is developed under a BSD license by \href{https://www.nlnetlabs.nl/}{NLnet Labs}.
|
||||
|
||||
The resolver process pledges \texttt{"stdio inet dns rpath"} and
|
||||
restricts access to the file system using unveil(2) to
|
||||
\texttt{/etc/ssl/cert.pem}. This is the process that is exposed to the
|
||||
Internet and handles untrusted data. It would be preferable to have
|
||||
one process exposed to the Internet and another to parse untrusted
|
||||
data but that is not possible to do with libunbound.
|
||||
|
||||
Since we are using a real recursive name server, that gives us many
|
||||
options on how we can resolve names:
|
||||
\begin{itemize}
|
||||
\item We can do our own recursion, walk down from the root zone using
|
||||
qname minimization to improve privacy.
|
||||
\item We can use the name server we learned from dhcpleased(8) and
|
||||
slaacd(8) as forwarders, so we do not need to do our own recursion,
|
||||
which might be faster.
|
||||
\item We can try to opportunistically speak DNS over TLS (DoT) to the
|
||||
learned name servers to prevent eavesdroppers from listening in.
|
||||
\item We can configure forwarders manually to not depend on the network
|
||||
provided name servers. Those might be more trustworthy. They can
|
||||
also be DoT forwarders to prevent eavesdropping.
|
||||
\item As a last resort, unwind(8) can behave exactly like the libc stub
|
||||
resolver\footnote{Call this the "Dutch train problem": the free Wi-Fi on
|
||||
Dutch trains do not like DNS queries with an \emph{EDNS0} option, they
|
||||
intercept them, do not understand them, and answer \emph{NXDOMAIN}. There
|
||||
are other free Wi-Fi networks that are similarly broken.}.
|
||||
\end{itemize}
|
||||
We call these resolving strategies and unwind(8) actively probes if
|
||||
they are usable by sending test queries when it notices that the
|
||||
network changed, for example because the laptop moved to a different Wi-Fi
|
||||
network or woke up from suspension. It then orders them by quality and
|
||||
picks the best one.
|
||||
|
||||
There is an implicit skew in the strategies for finding the best one:
|
||||
a manually configured DoT name server is always considered better than
|
||||
a name server provided by the local network, as long as it is available
|
||||
and not too slow.
|
||||
|
||||
unwind(8) is not too concerned about preserving privacy, it is
|
||||
pragmatic and tries to resolve names the best way it can, and it will use
|
||||
the local name servers provided by the network if those are the only ones available.
|
||||
|
||||
Since unwind(8) uses libunbound it also supports DNSSEC. DNSSEC
|
||||
provides data integrity and cryptographic authenticity, it does not
|
||||
provide confidentiality.
|
||||
|
||||
unwind(8) is pragmatic about DNSSEC. When it tests the quality of a
|
||||
resolving strategy it also tries to find out if DNSSEC is
|
||||
available. There are many reasons why DNSSEC might not be available: the
|
||||
network is misconfigured, DNSSEC is blocked or the laptop
|
||||
does not (yet) have the correct time. If DNSSEC does not work, unwind(8) does not insist on using it.
|
||||
|
||||
Of course this makes it susceptible to a downgrade attack. To mitigate
|
||||
this, unwind(8) will insist on DNSSEC working after it has discovered once
|
||||
that DNSSEC is working in the local network. This means that an
|
||||
attacker needs to be able to block DNSSEC from the moment we connect
|
||||
to a network. They cannot show up later and try to downgrade
|
||||
us. unwind(8) will only become lenient again when we connect to a new
|
||||
network.
|
||||
|
||||
This is not a strong mitigation of course, but DNSSEC is not a fix for
|
||||
everything at the resolver. Applications also need
|
||||
to do their part and decide how much they are willing to trust
|
||||
DNS. For example ssh(1)'s \emph{VerifyHostKeyDNS} feature will only trust
|
||||
host key fingerprints it obtained from DNS if they were validated
|
||||
using DNSSEC and the validator runs on the local
|
||||
machine\footnote{Technically not entirely true, ssh(1) trusts what libc
|
||||
indicates and libc automatically trusts localhost. See \emph{trust-ad} in
|
||||
\href{https://man.openbsd.org/resolv.conf.5}{resolv.conf(5)}.}. Otherwise it will ask the user what to do.
|
||||
|
||||
A worst-case scenario when joining a partially broken Wi-Fi network
|
||||
with captive portal and a manually configured DoT name server might
|
||||
look like this:
|
||||
\begin{enumerate}
|
||||
\item We connect to the network, we cannot reach the DoT name server and
|
||||
cannot do our own recursion.
|
||||
\item unwind(8) will choose the name server provided by the
|
||||
network. It also notes that we just connected to a new network so
|
||||
it is lenient with respect to DNSSEC validation. In effect it will
|
||||
ignore validation errors.
|
||||
\item We try to access a website and the captive portal
|
||||
detection in the browser triggers. We click the buttons and fill in
|
||||
the forms until we are allowed on the internet.
|
||||
\item unwind(8) notices that it can do its own recursion.
|
||||
\item At the same time, unwind(8) notices that the DoT name server is
|
||||
also reachable now and starts using it.
|
||||
\end{enumerate}
|
||||
|
||||
unwind(8) does not natively support DNS over HTTPS (DoH) and we
|
||||
sometimes find ourselves in networks that block everything except for
|
||||
TCP port 443. One way around this is to use dnscrypt-proxy from ports
|
||||
which does support DoH. We can point unwind(8) at it by manually
|
||||
configuring a plain DNS forwarder in addition to a DoT forwarder:
|
||||
\begin{verbatim}
|
||||
$ cat /etc/unwind.conf
|
||||
forwarder "9.9.9.9" port 853 \
|
||||
authentication name "dns.quad9.net" DoT
|
||||
forwarder "2620:fe::9" port 853 \
|
||||
authentication name "dns.quad9.net" DoT
|
||||
# dnscrypt-proxy for DoH
|
||||
forwarder "127.0.0.1" port 5353
|
||||
\end{verbatim}
|
||||
|
||||
\section{Time for gelato}
|
||||
\label{sec:orgbb01a19}
|
||||
There are various transition technologies that get us from an IPv4-only
|
||||
Internet to an IPv6-only Internet. We will only look at \emph{NAT64},
|
||||
\emph{DNS64}, and \emph{464XLAT}.
|
||||
|
||||
\emph{NAT64} allows us to reach IPv4 hosts from an IPv6-only network by
|
||||
pretending that the hosts are IPv6 enabled. IPv6 addresses are so large
|
||||
that we can easily encode all of IPv4 in an IPv6 /64 prefix, which is
|
||||
the usual size of on IPv6 prefix we see per layer-two network. In fact
|
||||
we don't need the whole /64, a /96 is enough to encode the whole IPv4
|
||||
Internet.
|
||||
|
||||
Let us pretend we know the /96 prefix used for \emph{NAT64} and the IPv4
|
||||
address we want to reach. Forming an IPv6 address for the host is then
|
||||
simply a bitwise-or operation of the IPv4 address with the /96 prefix,
|
||||
the IPv4 address fills in the lower bits of the IPv6 prefix. This is
|
||||
called address synthesis.
|
||||
|
||||
We can then use this address to connect to the IPv4-only
|
||||
host. Somewhere on the network path is the \emph{NAT64} gateway that is
|
||||
dual stacked. It knows that our packets are using \emph{NAT64} because it
|
||||
is configured with the /96 prefix. It intercepts the packets and forms
|
||||
IPv4 packets and sends them on their way. The gateway needs to be
|
||||
stateful to be able to \emph{NAT} the return traffic back to us.
|
||||
|
||||
We use DNS to find out the IPv4 address that we want to connect to. The
|
||||
local name servers that slaacd(8) learned about would know
|
||||
about the \emph{NAT64} prefix used in the network and do the address
|
||||
synthesis for us. This is called \emph{DNS64}. The problem with this is that
|
||||
the name servers spoof DNS answers, something that DNSSEC tries to prevent.
|
||||
unwind(8) will detect this and generate an error, or
|
||||
unwind(8) might not even talk to the designated name servers at all.
|
||||
|
||||
To get around this, unwind(8) itself can detect the presence of \emph{DNS64}
|
||||
on a network by asking the local name servers for the \emph{AAAA} record,
|
||||
i.e. the IPv6 address, for something that is guaranteed to never have
|
||||
one: \emph{ipv4only.arpa}. If it gets an answer, it can reverse the address
|
||||
synthesis and learn the \emph{NAT64} prefix. With that information it can
|
||||
do \emph{DNS64} itself and there is no longer a problem with DNSSEC.
|
||||
|
||||
The downsides of this mechanism are that it is quite complicated, it
|
||||
messes around with DNS, and it does not work with IPv4 address
|
||||
literals. It also does not work with programs that are fundamentally
|
||||
IPv4-only: \texttt{ping example.com} will never work in an IPv6-only network
|
||||
with only \emph{NAT64 / DNS64}.
|
||||
|
||||
Instead of pretending that the IPv4 host we want to reach has IPv6, we can
|
||||
pretend to have working IPv4 if a \emph{NAT64} gateway is present. We ask
|
||||
the kernel via the \href{https://man.openbsd.org/pf.4}{pf(4)} firewall to do the IPv4-to-IPv6 translation
|
||||
for us. The \emph{NAT64} gateway will then do the reverse translation and
|
||||
send an IPv4 packet on its way. This is called \emph{464XLAT}.
|
||||
|
||||
We first need an IPv4 address, RFC 7335 reserved \texttt{192.0.0.0/29} for
|
||||
this purpose:
|
||||
\begin{verbatim}
|
||||
ifconfig pair1 inet 192.0.0.4/29
|
||||
\end{verbatim}
|
||||
We then need a default gateway:
|
||||
\begin{verbatim}
|
||||
ifconfig pair2 rdomain 1
|
||||
ifconfig pair2 inet 192.0.0.1/29
|
||||
\end{verbatim}
|
||||
Because pf(4) will only do address family translation on inbound rules
|
||||
we need a different \emph{rdomain} and use \href{https://man.openbsd.org/pair.4}{pair(4)} interfaces. We need to
|
||||
connect them:
|
||||
\begin{verbatim}
|
||||
ifconfig pair1 patch pair2
|
||||
\end{verbatim}
|
||||
And then we can configure our default route:
|
||||
\begin{verbatim}
|
||||
route add -host -inet default 192.0.0.1 \
|
||||
-priority 48
|
||||
\end{verbatim}
|
||||
We set it to a very low priority\footnote{Remember that a high priority
|
||||
\textbf{value} means low priority.} so that it does not interfere with routes
|
||||
that dhcpleased(8) configures when we move to an IPv4 enabled network.
|
||||
|
||||
We then need to configure address family translation in pf(4) when we
|
||||
detect \emph{NAT64} being present. This is were \href{https://github.com/fobser/gelatod/}{gelatod(8)} comes in. It is
|
||||
a Customer-side transLATor (\emph{CLAT}) configuration daemon. \emph{CLAT}
|
||||
is what \emph{464XLAT} calls the address translation happening on the laptop.
|
||||
|
||||
gelatod(8) is yet another privilege-separated daemon\footnote{At this point
|
||||
we will not go into pledge details.} that checks for the presence of a \emph{NAT64}
|
||||
gateway whenever we change networks. It does so either via the
|
||||
\emph{ipv4only.arpa} trick or explicitly via router advertisements. RFC
|
||||
8781 specifies how a network can signal the presence of a \emph{NAT64}
|
||||
gateway.
|
||||
|
||||
gelatod(8) needs a pf(4) anchor into which it adds rules that are
|
||||
similar to this example:
|
||||
\begin{verbatim}
|
||||
pass in log quick on pair2 inet \
|
||||
af-to inet6 \
|
||||
from 2001:db8::da68:f613:4573:4ed0 \
|
||||
to 64:ff9b::/96 \
|
||||
rtable 0
|
||||
\end{verbatim}
|
||||
The rule is doing address family translation to IPv6 on incoming
|
||||
packets on \texttt{pair2}. In this example it uses
|
||||
\texttt{2001:db8::da68:f613:4573:4ed0} as the IPv6 source address, gelatod(8)
|
||||
learned this from the system when slaacd(8) configured
|
||||
it. \texttt{64:ff9b::/96} is the learned \emph{NAT64} prefix and we are moving
|
||||
traffic back to \texttt{rtable 0}. Remember \texttt{pair2} is in rdomain 1.
|
||||
|
||||
While this works rather well, it is also complicated to set up,
|
||||
which is why gelatod(8) is not in
|
||||
OpenBSD base but lives in ports. We believe in good defaults in
|
||||
OpenBSD and try to make it easy for the user.
|
||||
|
||||
\section{Future work}
|
||||
\label{sec:org3bacedc}
|
||||
We would like to have the functionality of gelatod(8) in OpenBSD base.
|
||||
gelatod(8) was mostly a proof of concept and we imagine that a new network device
|
||||
like clat(4) would take over the role of client side address family
|
||||
translation. It could be always present and gelatod(8) would just enable
|
||||
and disable it. At that point we could move the functionality into
|
||||
slaacd(8) and delete gelatod(8). \emph{CLAT} is defined as a stateless
|
||||
mechanism so it does not need the full pf(4) machinery for address
|
||||
family translation.
|
||||
|
||||
It would be valuable to have DNS over HTTPS (DoH) and DNS over Quic (DoQ)
|
||||
natively in unwind(8). We are mostly waiting on upstream to implement
|
||||
support in unbound(8).
|
||||
|
||||
There are also minor issues that could be improved:
|
||||
\begin{itemize}
|
||||
\item The captive portal detection in unwind(8) is not perfect and could be improved upon.
|
||||
\item dhcpleased(8) and slaacd(8) should remember IP addresses from
|
||||
networks they have been connected to previously, to be able to quickly
|
||||
re-establish connectivity by probing whether we are connecting to a
|
||||
previous network while the lifetime of our addresses have not expired
|
||||
yet. RFC 4436 "Detecting Network Attachment in IPv4 (DNAv4)" and RFC
|
||||
6059 "Simple Procedures for Detecting Network Attachment in IPv6"
|
||||
discuss the details.
|
||||
\item It would be helpful if the dhcpleased(8) parent process could be
|
||||
pledged. This is currently not possible because of bpf(4). Things to
|
||||
investigate here are changes to the network stack that would allow
|
||||
us to use raw sockets instead of bpf(4) sockets or the ability to
|
||||
\href{https://man.openbsd.org/dup.2}{dup(2)} an existing bpf(4) socket and reprogram the interface it is
|
||||
using.
|
||||
\end{itemize}
|
||||
\section{Conclusion}
|
||||
\label{sec:org29b4bac}
|
||||
In this paper we have described how OpenBSD improved the user
|
||||
experience and security of laptop users when visiting diverse network
|
||||
locations. The system remembers Wi-Fi networks and automatically
|
||||
connects to them. It automatically discovers when the network changes
|
||||
and acquires new IPv4 and IPv6 addresses or renews existing
|
||||
configurations. OpenBSD also actively probes available DNS resolving
|
||||
strategies and picks the best one available. Privilege-separation and
|
||||
restricted service operating mode ensure that untrusted data is parsed
|
||||
with the least privileges necessary, protecting the rest of the
|
||||
system.
|
||||
\section*{Acknowledgment}
|
||||
The author would like to thank Mine Temuerhan for copyediting the paper.
|
||||
\end{document}
|
@ -36,6 +36,7 @@
|
||||
- [[http://www.openbsd.org/papers/bsdcan2019_unwind.pdf][2019-05-17, BSDCan: unwind(8) a privilege-separated, validating DNS recursive nameserver for every laptop]], [[https://www.youtube.com/watch?v=88SoI49nO4o][video]]
|
||||
- [[https://archive.fosdem.org/2020/schedule/event/dns_unwind/][2020-02-01, FOSDEM: unwind(8) a privilege-separated, validating DNS recursive nameserver for every laptop]]
|
||||
- [[https://undeadly.org/cgi?action=article;sid=20200922090542][2020-09-21, undeadly: k2k20 hackathon report: Florian Obser on DNS]]
|
||||
- [[https://2023.asiabsdcon.org/program.html.en][2023-04-02, AsiaBSDCon 2023: P09B: Dynamic Host Configuration, please]] ([[https://www.openbsd.org/papers/asiabsdcon2023-dynamic_host_configuration_please-slides.pdf][slides]]), ([[file:asiabsdcon2023/asiabsdcon_2023_final.pdf][paper]])
|
||||
|
||||
* meta
|
||||
This page is made using [[https://orgmode.org/][Org Mode]]'s [[https://orgmode.org/manual/Publishing.html][publishing]] facility. The
|
||||
|
Loading…
Reference in New Issue
Block a user