1# This file is part of rex - a remote execution utility
2# Copyright (C) 2012, 2013, 2015, 2016 Sergey Poznyakoff
3#
4# Rex is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 3, or (at your option)
7# any later version.
8#
9# Rex is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with Rex.  If not, see <http://www.gnu.org/licenses/>.
16
17# For instructions and a list of configurable variables, see the description
18# of startvpnc procedure below.
19
20package provide Vpnc 1.0
21
22package require Tcl 8.5
23package require Expect 5.44.1.15
24package require Entrustcard 1.0
25
26namespace eval ::vpnc {
27    namespace export checkpid start
28    namespace ensemble create
29
30    # Store the credential "what" for the connection "id" in the variable
31    # "retName".  Return 1 on success, 0 on failure.
32    #
33    # This function always returns failure.  The user is supposed to
34    # override it if the need be.
35    #
36    proc get_credentials {id what retName} {
37#	upvar $retName ret
38	return 0
39    }
40
41    variable vpnc_command "/usr/local/sbin/vpnc"
42    variable pidof_command "/sbin/pidof"
43
44    proc checkpid {pidfile} {
45	variable cleanup_command
46	variable pidof_command
47
48	if ![file exists $pidfile] {
49	    return 0
50	}
51	set fd [open $pidfile "r"]
52	set pid [gets $fd]
53	close $fd
54	if {[catch {exec $pidof_command vpnc 2>@1} res] ||
55	    [lsearch [split $res] $pid] == -1} {
56	    if {[info exists cleanup_command] &&
57		[file executable $cleanup_command]} {
58		exec sudo $cleanup_command $pidfile
59	    } else {
60		exec sudo /usr/bin/rm $pidfile
61	    }
62	    return 0
63	}
64	return 1
65    }
66
67    # start ID CFGFILE PIDFILE
68    #
69    # Start VPN identified by ID, using configuration file CFGFILE and
70    # PID file PIDFILE.
71    #
72    # If the VPN server requires Entrust card authentication, the variable
73    # vpnc::carddir must be set to the directory keeping card files. Each
74    # file must be stored in a file named by its number. See the description of
75    # the procedure readcards in file entrustcard.tcl for its format.
76    #
77    # The current user must be able to execute VPNC binary (i.e.
78    # $vpnc_command) via sudo without password. To remove stale PID files,
79    # they must be able to run /usr/bin/rm on files in VPNC
80    # state directory. This too requires a sudo privilege, which is normally
81    # set in /etc/sudoers as follows:
82    #
83    #   VPNC  ALL=(ALL) NOPASSWD: /usr/bin/rm /var/run/vpnc/*.pid
84    #
85    # In case it is regarded as a too risky permission, set variable
86    # vpnc::cleanup_command to the full pathname of a wrapper script
87    # to be run instead of rm. The script should accept the full
88    # pathname of a PID file as its argument and remove it if it sees fit. An
89    # example vpnc-cleanup script is supplied in the rex distribution.
90    #
91    proc start {args} {
92	variable carddir
93	variable vpnc_command
94
95	while {[llength $args] > 0} {
96	    switch -regexp -- [lindex $args 0] {
97		{^-user$} {
98		    set username [lindex $args 1]
99		    set args [lreplace $args [set args 0] 1]
100		}
101		{^-pass$} {
102		    set password [lindex $args 1]
103		    set args [lreplace $args [set args 0] 1]
104		}
105		{^--$} { break }
106		{^-.*} {
107		    return -code error "unknown option [lindex $args 0]"
108		}
109		default { break }
110	    }
111	}
112
113	set id [lindex $args 0]
114	set cfgfile [lindex $args 1]
115	set pidfile [lindex $args 2]
116
117	if [checkpid $pidfile] {
118	    return
119	}
120
121	puts "Starting vpn $id"
122
123	set password_sent 0
124
125	spawn -ignore HUP -noecho sudo $vpnc_command --local-port 0 --pid-file $pidfile $cfgfile
126
127	expect {
128	    "Enter a response to the grid challenge*\n" {
129		if {![regexp {Enter a response to the grid challenge \[([[:alnum:]])([[:alnum:]])\] \[([[:alnum:]])([[:alnum:]])\] \[([[:alnum:]])([[:alnum:]])\] using a card with serial number ([[:digit:]]+)\.$} [string trimright $expect_out(0,string)] dummy x1 y1 x2 y2 x3 y3 n]} {
130		    close
131		    return -code error "$id: cannot parse reply string: $expect_out(0,string)"
132		}
133
134		if {![info exist carddir]} {
135		    return -code error "VPN \"$id\" requires entrustcard authentication (card $n), but carddir is not set"
136		}
137
138		if {[entrustcard load "$carddir/$n" card]} {
139		    exit 1
140		}
141
142		set reply [entrustcard decode $expect_out(0,string) card]
143		exp_continue
144	    }
145	    -re {username for.*:[[:space:]]*$} {
146		if {[info exist username] || \
147			[get_credentials $id username username]} {
148		    send "$username\r"
149		    exp_continue
150		} else {
151		    close
152		    return -code error "$id: username required but not supplied"
153		}
154	    }
155	    -re {assword for.*:[[:space:]]*$} {
156		# VPNC uses getpass(3), which needs some time to open
157		# /dev/tty and alter tty settings.  The code below waits
158		# for one second before sending the reply.  Hopefully this
159		# is enough for the things to settle.
160		if {[info exist reply]} {
161		    sleep 1
162		    send "$reply\r"
163		    unset reply
164		    incr password_sent
165		} elseif {$password_sent == 0} {
166		    if {[info exist password] || \
167			    [get_credentials $id password password]} {
168			sleep 1
169			send "$password\r"
170			incr password_sent
171		    } else {
172			close
173			return -code error "$id: password required but not supplied"
174		    }
175		} elseif {$password_sent == 1} {
176		    if {[get_credentials $id password password]} {
177			close
178			return -code error "$id: password not accepted"
179		    }
180		    sleep 1
181		    send "$password\r"
182		    incr password_sent
183		} else {
184		    close
185		    return -code error "$id: password not accepted"
186		}
187		exp_continue
188	    }
189	    "VPNC started*"   {
190		puts -nonewline $expect_out(0,string)
191	    }
192	    timeout {
193		close
194		return -code error "$id: timed out"
195	    }
196	}
197    }
198    wait -nowait
199}
200