Road Warrior VPN with MAC OS X and Nat-Traversal
This info here is basically a follow-up to my document in 2004, but with the primary focus on centralized authentication with LDAP and radius, MAC OS X and NAT-T.
If you want to know the gory details, please read my document in 2004 and jacco's great IPSec-Guide. Other than that, I've fought some time to get THIS HERE going, I didn't find much info on the net about MAC OS X Road Warriors behind NAT-T with a classless static routing setup, that's why I'm basically writing up samples of my config-files for you here. I'm sorry for the incompleteness of explanations (though my posted config-files are complete). But at least you can find tons of those (explanations) in the net.
Software Choices
The whole setup is based on Debian Etch, with some tools needed to compile by hand - unfortunately. This guide is not for beginners, I won't go into much detail.
- IPSec-sofware: kernel 2.6 and openswan
- l2tp-daemon: l2tpns
- LDAP-daemon: slapd (openldap)
- radius-daemon: freeradius
- dhcp-daemon: isc dhcpd
Authentication (LDAP + Radius)
I'm keeping all my user data in a centralized LDAP database. Authentication PPP against LDAP is a hard thing, so I decided to also implement a radius daemon that queries the LDAP daemon for authentication data, so I can let the ppp-daemons ask the super-supported radius-servers. This is the easy part. I won't tell you how to setup an LDAP-database, go and read for yourself.
Adding a radius-server to an existing LDAP-database simply for authentication is - fortunately - VERY easy. All I've modified is the ldap-sections. Here's my config (I didn't touch the ldap.attrmap btw):
radiusd.conf:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
prefix = /usr
exec_prefix = /usr
sysconfdir = /etc
localstatedir = /var
sbindir = ${exec_prefix}/sbin
logdir = /var/log/freeradius
raddbdir = /etc/freeradius
radacctdir = ${logdir}/radacct
confdir = ${raddbdir}
run_dir = ${localstatedir}/run/freeradius
log_file = ${logdir}/radius.log
libdir = /usr/lib/freeradius
pidfile = ${run_dir}/freeradius.pid
user = freerad
group = freerad
max_request_time = 30
delete_blocked_requests = no
cleanup_delay = 5
max_requests = 1024
bind_address = *
port = 0
hostname_lookups = no
allow_core_dumps = no
regular_expressions = yes
extended_expressions = yes
log_stripped_names = no
log_auth = no
log_auth_badpass = no
log_auth_goodpass = no
usercollide = no
lower_user = no
lower_pass = no
nospace_user = no
nospace_pass = no
checkrad = ${sbindir}/checkrad
security {
max_attributes = 200
reject_delay = 1
status_server = no
}
proxy_requests = no
$INCLUDE ${confdir}/clients.conf
snmp = no
$INCLUDE ${confdir}/snmp.conf
thread pool {
start_servers = 5
max_servers = 32
min_spare_servers = 3
max_spare_servers = 10
max_requests_per_server = 0
}
modules {
pap {
encryption_scheme = crypt
}
chap {
authtype = CHAP
}
pam {
pam_auth = radiusd
}
unix {
cache = no
cache_reload = 600
shadow = /etc/shadow
radwtmp = ${logdir}/radwtmp
}
$INCLUDE ${confdir}/eap.conf
mschap {
}
ldap {
server = "my.ldap.server.fqdn"
identity = "cn=radiusdaemon,dc=my,dc=company"
password = secret
basedn = "o=searchBase,dc=my,dc=company"
filter = "(&(serviceName=radius)(uid=%{Stripped-User-Name:-%{User-Name}}))"
base_filter = "(serviceName=radius)"
start_tls = no
dictionary_mapping = ${raddbdir}/ldap.attrmap
ldap_connections_number = 5
password_attribute = userPassword
edir_account_policy_check=no
timeout = 4
timelimit = 3
net_timeout = 1
compare_check_items = no
set_auth_type = yes
}
realm suffix {
format = suffix
delimiter = "@"
ignore_default = no
ignore_null = no
}
realm realmpercent {
format = suffix
delimiter = "%"
ignore_default = no
ignore_null = no
}
realm ntdomain {
format = prefix
delimiter = "\\"
ignore_default = no
ignore_null = no
}
checkval {
item-name = Calling-Station-Id
check-name = Calling-Station-Id
data-type = string
}
preprocess {
huntgroups = ${confdir}/huntgroups
hints = ${confdir}/hints
with_ascend_hack = no
ascend_channels_per_line = 23
with_ntdomain_hack = no
with_specialix_jetstream_hack = no
with_cisco_vsa_hack = no
}
files {
usersfile = ${confdir}/users
acctusersfile = ${confdir}/acct_users
preproxy_usersfile = ${confdir}/preproxy_users
compat = no
}
detail {
detailfile = ${radacctdir}/%{Client-IP-Address}/detail-%Y%m%d
detailperm = 0600
}
acct_unique {
key = "User-Name, Acct-Session-Id, NAS-IP-Address, Client-IP-Address, NAS-Port"
}
$INCLUDE ${confdir}/sql.conf
radutmp {
filename = ${logdir}/radutmp
username = %{User-Name}
case_sensitive = yes
check_with_nas = yes
perm = 0600
callerid = "yes"
}
radutmp sradutmp {
filename = ${logdir}/sradutmp
perm = 0644
callerid = "no"
}
attr_filter {
attrsfile = ${confdir}/attrs
}
counter daily {
filename = ${raddbdir}/db.daily
key = User-Name
count-attribute = Acct-Session-Time
reset = daily
counter-name = Daily-Session-Time
check-name = Max-Daily-Session
allowed-servicetype = Framed-User
cache-size = 5000
}
sqlcounter dailycounter {
counter-name = Daily-Session-Time
check-name = Max-Daily-Session
sqlmod-inst = sql
key = User-Name
reset = daily
query = "SELECT SUM(AcctSessionTime - \
GREATEST((%b - UNIX_TIMESTAMP(AcctStartTime)), 0)) \
FROM radacct WHERE UserName='%{%k}' AND \
UNIX_TIMESTAMP(AcctStartTime) + AcctSessionTime > '%b'"
}
sqlcounter monthlycounter {
counter-name = Monthly-Session-Time
check-name = Max-Monthly-Session
sqlmod-inst = sql
key = User-Name
reset = monthly
query = "SELECT SUM(AcctSessionTime - \
GREATEST((%b - UNIX_TIMESTAMP(AcctStartTime)), 0)) \
FROM radacct WHERE UserName='%{%k}' AND \
UNIX_TIMESTAMP(AcctStartTime) + AcctSessionTime > '%b'"
}
always fail {
rcode = fail
}
always reject {
rcode = reject
}
always ok {
rcode = ok
simulcount = 0
mpp = no
}
expr {
}
digest {
}
exec {
wait = yes
input_pairs = request
}
exec echo {
wait = yes
program = "/bin/echo %{User-Name}"
input_pairs = request
output_pairs = reply
}
ippool main_pool {
range-start = 192.168.1.1
range-stop = 192.168.3.254
netmask = 255.255.255.0
cache-size = 800
session-db = ${raddbdir}/db.ippool
ip-index = ${raddbdir}/db.ipindex
override = no
maximum-timeout = 0
}
}
instantiate {
exec
expr
}
authorize {
preprocess
suffix
files
ldap
}
authenticate {
Auth-Type LDAP {
ldap
}
}
preacct {
preprocess
acct_unique
suffix
files
}
accounting {
detail
unix
radutmp
}
session {
radutmp
}
post-auth {
}
pre-proxy {
}
post-proxy {
eap
}
And here's my client.conf:
1
2
3
4
5
6
client 1.2.3.4 {
secret = anothersecret
shortname = your.vpn.server
}
# 1.2.3.4 is the IP of my VPN-Server
This is it. You can test radius-connectivity with the binary radtest. If radiusToLDAP-Authentication doesn't work, don't go any further, it needs to work, my setup is based on radius.
IPSec
One of the hardest parts (because it's hard to debug) was getting IPSec to run. I won't comment much here, read everything about it in my other document and of course on jacco's great IPSec-documentation.
ipsec.conf of openswan/debian:
1
2
3
4
5
6
7
8
config setup
nat_traversal=yes
nhelpers=0
forwardcontrol=yes
include /etc/ipsec.d/examples/mac.conf
include /etc/ipsec.d/examples/l2tp-psk.conf
include /etc/ipsec.d/examples/no_oe.conf
mac.conf:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
conn L2TP-PSK-NAT-OSX
authby=secret
forceencaps=yes
pfs=no
auto=add
keyingtries=3
dpdtimeout=60
dpdaction=clear
rekey=no
left=%defaultroute
leftprotoport=17/1701
right=%any
rightprotoport=17/%any
rightsubnet=vhost:%priv,%no
l2tp-psk.conf:
1
2
3
4
5
6
7
8
9
10
11
12
conn L2TP-PSK-noNAT
authby=secret
pfs=no
auto=add
keyingtries=3
rekey=no
type=transport
left=%defaultroute
leftprotoport=17/1701
right=%any
rightprotoport=17/1701
rightsubnet=vhost:%priv,%no
Hint: l2tp-psk.conf will never be chosen by openswan, but this is the file that makes your windows machines fly. If you are configuring your stuff for windows xp (didn't test with weird Vista) this is the file to go.
ipsec.secrets (yes, this setup is NOT certificate-based):
1
1.2.3.4 %any: PSK "your-psk-here"
Note: 1.2.3.4 is the IP of your VPN-Server.
The hard l2tpns-part…
l2tpns is some kind of beast one has to deal with. It's debugging was somehow weird, but once it works, it offers tons of features and really works fast (compared to l2tpd and such). Why l2tpns? Because l2tpd is not developed any further, just bugfixed. l2tpns has integrated radius support and much more (like bgp. clustering and so on - stuff we won't need here). Attention: On my installation l2tpns needs more than 60MB of Memory. That's a lot. Don't use l2tpns on an embedded device.
Problems: l2tpns wouldn't work for me for a long time. The reason was that the OS X implementation of l2tp and the l2tpns implementation didn't really shake hands well. l2tpns sent a hello message where OS X expected something else (namely: L2TP received invalid message (expected ICRP, received Hello)).
The hackish way around that was to comment out a few lines in the source lf l2tpns.c:
1
2
3
4
5
6
7
8
9
10
2837 // Send hello
2838 /*if (tunnel[t].state == TUNNELOPEN && !tunnel[t].controlc && \
(time_now - tunnel[t].lastrec) > 60)
2839 {
2840 controlt *c = controlnew(6); // sending HELLO
2841 controladd(c, 0, t); // send the message
2842 LOG(3, 0, t, "Sending HELLO message\n");
2843 t_actions++;
2844 } */
2845
Don't forget to recompile your l2tpns (you can get the source via apt-get source l2tpns, change that stuff, recompile the l2tpns daemon and just copy the binary over your existing binary).
and here's my l2tpns.config (called startup-config):
1
2
3
4
5
6
7
8
9
10
11
12
13
set debug 1
set log_file "/var/log/l2tpns"
set pid_file "/var/run/l2tpns.pid"
set l2tp_secret "secret"
set primary_dns 1.2.3.5
set primary_radius 1.2.3.3
set primary_radius_port 1812
set secondary_radius_port 1812
set radius_secret "my-radius-secret"
set radius_authtypes "pap"
set radius_accounting no
set accounting_dir "/var/run/l2tpns/acct"
set peer_address 1.1.1.1
Info: The auth-type is pap, which is sent unencrypted through the internet, but the IPSec encapsulation takes care of that, so there's nothing to fear. Additionally my ldap-db only stores encrypted passwords, so using chap is not possible.
Additionally I've defined some private IP-Adresses in the file ip_pool:
1
10.1.255.0/24
Info: l2tpns includes an pppd, so you don't need to setup pppd, l2tpns uses just one interface called 'tun0' (you need tun/tap support in the kernel for that). You can put firewall-rules on that interface if you think that's a good idea. Furthermore if your other communication partners don't know nothing about this private VPN-Network-Range (10.1.255.0/24), you should put a MASQUERADE-Rule in your iptables-Ruleset:
1
iptables -t nat -A POSTROUTING -s 10.1.255.0/24 -j MASQUERADE
Good. This is great. Everything should work by now. If your IPSec Setup doesn't work yet (it doesn't say "ESTABLISHED" in the logs) don't mess with l2tpdns. If you can connect now and say - wow, that's awesome - it works! You probably are still looking for one thing (at least I was looking for that): You only want to have SPECIFIC routes to your VPN Machine. In my case I have a private Network somewhere out there, where I can only get via VPN. But as for the rest of the Internet routes I want to use my existing connection (not VPN). One way to do this is to alter the routing table of my Mac OS X:
1
sudo route add -net 1.2.3.0/28 1.1.1.1
Dumb. I want that route automagically. How?
DHCPD
It's not really documented, but it works. OS X sends an DHCP INFORMATIONAL Message via the VPN Connection to the l2tp-Server - so to say the tun0 interface on the server side gets a DHCP-Request one can act upon. The OS X Client requests domain name, domain name servers and others - such as static (classless) routes. This feature seems to be pretty new, but isc dhcp3-server seems to support every dhcp-request property with this little mechanism here:
my dhcpd.conf:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ddns-update-style none;
option domain-name "wogri.at";
option domain-name-servers dns.wogri.at;
default-lease-time 600;
max-lease-time 7200;
authoritative;
log-facility local7;
option ms-classless-static-routes code 249 = array of unsigned integer 8;
option ms-classless-static-routes 28, 1,2,3,0, 1,1,1,1;
option routers 1.1.1.1;
class "vpn-clients" {
match if option agent.circuit-id = "tun0";
}
subnet 0.0.0.0 netmask 0.0.0.0 {
pool {
range 192.168.1.50 192.168.1.254;
default-lease-time 3600;
max-lease-time 7200;
}
}
Can you see what I've done?
option ms-classless-static-routes 28, 1,2,3,0, 1,1,1,1; means the route to 1.2.3.0/28 can be reached through gateway 1.1.1.1. The OS X Client happily eats this, and your routes are out there. The range parameter is ignored, OS X doesn't ask for it in it's INFORMATIONAL request, so it's also not in the answering packed (I've sniffed this from the tun0 interface).
BUT WAIT! Once more, you have to patch the source-code to make this work. Unfortunately dhcpd can't bind to the tun0 interface, because this interface is not an ethernet interface (for which bind was written). (Yes, you could dhcp-relay stuff, but I couldn't make this work either without patching the source-code):
Edit includes/site.h und uncomment the following:
1
138 #define USE_SOCKETS
Note: the '#' is not the comment-marker, you have to remove the /* and */ to uncomment the stuff [for all you script-hackers].
Recompile dhcpd, edit /etc/default/dhcpd and tell it to bind to tun0 and add some options for (debian-conforming) paths to dhcpd:
1
OPTIONS="-cf /etc/dhcp3/dhcpd.conf -lf /var/lib/dhcp3/dhcpd.leases -pf\ /var/run/dhcpd.pid"
Finally, tell the dhcpd startup-script to implement those paths:
/etc/init.d/dhcp3-server:
1
2
3
4
66 echo -n "Starting $DESC: "
67 echo $OPTOINS $INTERFACES
68 start-stop-daemon --start --quiet --pidfile $DHCPDPID \
69 --exec /usr/sbin/dhcpd3 -- -q $OPTIONS $INTERFACES
That's it, you're done!
OS X Client
This setup works and has been tested with leopard (10.5) and tiger (10.4, yet tiger doesn't ask for DHCP routes). I know Windows XP works if you disable example/mac.conf in ipsec.conf (or you have more than one public IP available for your VPN-Service and do a little tweak the ipsec-setup.
Fortunately the client setup is super-easy.