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.

Letzte Änderung: somewhen in 2008