#!@RUBY@ # # dtcpc, Turmpet Dynamic Tunnel Configuration Protocol client # # # Copyright (c) 2000-2006 Hajimu UMEMOTO # All rights reserved. # # Copyright (C) 1999 WIDE Project. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the project nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # $Id: dtcpc.rb,v 1.3 2000/05/27 11:45:02 jinmei Exp $ # $Mahoroba: src/dtcp/dtcpc.rb,v 1.73 2006/01/08 18:24:01 ume Exp $ # require 'getopts' require "socket" require "md5" require 'syslog' TIMEOUT = 60 DEBUG = false PASSWDFILE = '@PREFIX@/etc/dtcpc.auth' PIDFILE = '/var/run/dtcpc.pid' UDP_TUNNEL_PORT = 4028 ROUTE_GATEWAY = 0 ROUTE_CHANGE = 1 ROUTE_INTERFACE = 2 ROUTE_IFP = 3 # NetBSD 1.6 or later #TUNIF = "gif0" #TUNIF_CLONING = true #TUNNEL_CREATE = 'ifconfig %s tunnel %s %s' #TUNNEL_DELETE = 'ifconfig %s deletetunnel' #ROUTE_METHOD = ROUTE_IFP # # NetBSD 1.5.x or earlier # and OpenBSD #TUNIF = "gif0" #TUNIF_CLONING = false #TUNNEL_CREATE = 'ifconfig %s tunnel %s %s' #TUNNEL_DELETE = 'ifconfig %s deletetunnel' #ROUTE_METHOD = ROUTE_CHANGE # # FreeBSD 4.6-RELEASE or later #TUNIF = "gif" #TUNIF_CLONING = true #TUNNEL_CREATE = 'ifconfig %s tunnel %s %s' #TUNNEL_DELETE = 'ifconfig %s deletetunnel' #ROUTE_METHOD = ROUTE_IFP ##ROUTE_METHOD = ROUTE_INTERFACE # # FreeBSD 4.4-RELEASE or 4.5-RELEASE #TUNIF = "gif" #TUNIF_CLONING = true #TUNNEL_CREATE = 'ifconfig %s tunnel %s %s' #TUNNEL_DELETE = 'ifconfig %s deletetunnel' #ROUTE_METHOD = ROUTE_GATEWAY # # FreeBSD 4.3-RELASE or earlier #TUNIF = "gif0" #TUNIF_CLONING = false #TUNNEL_CREATE = 'gifconfig %s %s %s' #TUNNEL_DELETE = 'gifconfig %s delete' #ROUTE_METHOD = ROUTE_GATEWAY # TUNIF = '@DTCPC_TUNIF@' TUNIF_CLONING = true TUNNEL_CREATE = 'ifconfig %s tunnel %s %s' TUNNEL_DELETE = 'ifconfig %s deletetunnel' ROUTE_METHOD = ROUTE_IFP def usage() $stderr.print "usage: #{File.basename($0)} [-cdDlnU] [-b udpport] [-i if] [-m mtu] [-p port] [-t tuntype] [-u username] [-A addr] [-R dest] [-P prefix-delegation] server\n" end class PDInfo attr_accessor :iface attr_accessor :slaid attr_accessor :hostid attr_accessor :prefixlen private def initialize(iface, slaid, hostid, prefixlen) @iface = iface @slaid = slaid @hostid = (hostid == '') ? nil : hostid @prefixlen = prefixlen ? prefixlen.to_i : 64 end end class PrefixDelegation PrefixNew = 1 PrefixExist = 2 PrefixNoChange = 3 def update(prefixes) if @pdinfo.size <= 0 return end prefixes.each { |prefix| if @prefixes.has_key?(prefix) @prefixes[prefix] = PrefixNoChange else @prefixes[prefix] = PrefixNew end } @prefixes.each { |prefix, state| case state when PrefixNew add_prefix(prefix) @prefixes[prefix] = PrefixExist when PrefixNoChange @prefixes[prefix] = PrefixExist else delete_prefix(prefix) @prefixes.delete(prefix) end } if !@rtadvd_running if !@forwarding_enabled @old_forwarding = `sysctl -n net.inet6.ip6.forwarding`.chop execute("sysctl -w net.inet6.ip6.forwarding=1") @forwarding_enabled = true end if !@rtadvd_disable @old_accept_rtadv = `sysctl -n net.inet6.ip6.accept_rtadv`.chop execute("sysctl -w net.inet6.ip6.accept_rtadv=0") execute("rtadvd #{@rtadvd_interfaces}") @rtadvd_running = true @rtadvd_invoked = true end end end def cleanup(*keep) if @pdinfo.size <= 0 return end if !keep[0] update([]) if @rtadvd_invoked open("/var/run/rtadvd.pid", 'r') { |p| Process.kill("SIGTERM", p.readline.to_i) } execute("sysctl -w net.inet6.ip6.accept_rtadv=#{@old_accept_rtadv}") @rtadvd_running = false @rtadvd_invoked = false end if @forwarding_enabled execute("sysctl -w net.inet6.ip6.forwarding=#{@old_forwarding}") @forwarding_enabled = false end end end private def initialize(pdinfos, *rtadvd_disable) @prefixes = {} @pdinfo = [] pdinfos.each { |pdinfo| iface, slaid, hostid, prefixlen = pdinfo.split('/') @pdinfo.push(PDInfo.new(Interface.new(iface), slaid, hostid, prefixlen)) } if @pdinfo.size <= 0 return end @forwarding_enabled = false @rtadvd_invoked = false @rtadvd_running = rtadvd_running? @rtadvd_interfaces = rtadvd_interfaces @rtadvd_disable = (rtadvd_disable[0] || @rtadvd_interfaces =~ /^\s*$/) end def rtadvd_running? `ps ax`.each { |s| if s.scan(/rtadvd/).size > 0 return true end } return false end def rtadvd_interfaces ifaces = [] @pdinfo.each { |pdinfo| if pdinfo.prefixlen < 128 ifaces.push(pdinfo.iface.name) end } return ifaces.uniq.join(' ') end def addr_to_nla(addr) nla_ary = addr.split(':')[0..3] (0..3).each { |i| if !nla_ary[i] || nla_ary[i] == '' nla_ary[i] = '0' end } return nla_ary end def to_addr(nla_ary, slaid, hostid) nla = [nla_ary[0..2], sprintf("%x", nla_ary[3].hex + slaid.hex)].join(':') if hostid addr = "#{nla}:#{hostid}" else addr = "#{nla}:: eui64" end return addr end def add_prefix(prefix) addr, prefixlen = prefix.split('/') execute("route add -inet6 #{addr} -prefixlen #{prefixlen} ::1 -reject") nla_ary = addr_to_nla(addr) @pdinfo.each { |pdinfo| hostid = pdinfo.hostid ? pdinfo.hostid : pdinfo.iface.hostid addr = to_addr(nla_ary, pdinfo.slaid, hostid) pdinfo.iface.addaddr("#{addr} prefixlen #{pdinfo.prefixlen}") } end def delete_prefix(prefix) addr, prefixlen = prefix.split('/') execute("route delete -inet6 #{addr} -prefixlen #{prefixlen} ::1 -reject") nla_ary = addr_to_nla(addr) @pdinfo.each { |pdinfo| hostid = pdinfo.hostid ? pdinfo.hostid : pdinfo.iface.hostid addr = to_addr(nla_ary, pdinfo.slaid, hostid) pdinfo.iface.deladdr("#{addr} prefixlen #{pdinfo.prefixlen}") } end end def daemon(nochdir, noclose) pid = fork if pid == -1 return -1 elsif pid != nil exit 0 end Process.setsid() Dir.chdir('/') if (nochdir == 0) if noclose == 0 devnull = open("/dev/null", "r+") $stdin.reopen(devnull) $stdout.reopen(devnull) $stderr.reopen(devnull) end return 0 end def authenticate(user, seed, pass) m = MD5.new(user) m.update(seed) m.update(pass) return m.digest.unpack("H32")[0].tr('a-f', 'A-F') end def logmsg(msg) if $syslog.opened? $syslog.notice('%s', msg) else $stderr.print msg end end def debugmsg(msg) logmsg(msg) if ($debug) end def execute(cmd) logmsg("#{cmd}\n") system(cmd) end def getpassword_file(dst, user) if not File.exist?(PASSWDFILE) debugmsg("no authinfo file found\n") return nil end if not File.readable?(PASSWDFILE) debugmsg("no permission to read authinfo file\n") return nil end open(PASSWDFILE, 'r') { |p| p.each_line { |l| d, u, passwd = l.chop.split(":") if d == dst && u == user debugmsg("ok, relevant authinfo item found\n") return passwd end } } debugmsg("no relevant authinfo item found\n") return nil end def getpassword(dst, username) password = getpassword_file(dst, username) if password == nil open('/dev/tty', 'r') { |tty| system("stty -echo") $stderr.print "password for #{username}: " password = tty.readline } system("stty sane") $stderr.print "\n" end return password.chomp end class Route def setup(dstif) case @type when "static" @static_routes.split(/\s*,\s*/).each { |static_route| execute("route delete -inet6 #{static_route} > /dev/null 2>&1") cmd = route_add(static_route, dstif) if !cmd exit 1 end execute(cmd) } when "solicit" execute("rtsol #{dstif.name}") end end def delete if @type == "static" @static_routes.split(/\s*,\s*/).each { |static_route| execute("route delete -inet6 #{static_route}") } end end private def initialize(type, static_routes) @type = type @static_routes = static_routes end def route_add(dest, tunif) case ROUTE_METHOD when ROUTE_CHANGE cmd = "route add -inet6 #{dest} ::1; route change -inet6 #{dest} -ifp #{tunif.name}" when ROUTE_INTERFACE cmd = "route add -inet6 #{dest} -interface #{tunif.name}" when ROUTE_IFP cmd = "route add -inet6 #{dest} ::1 -ifp #{tunif.name}" else laddr = tunif.linklocal if !laddr logmsg("FATAL: cannot get link-local address of #{tunif.name}\n") return nil end cmd = "route add -inet6 #{dest} #{laddr}" end return cmd end end class Interface attr :name def up execute("ifconfig #{@name} up") end def down execute("ifconfig #{@name} down") end def addaddr(addr) execute("ifconfig #{@name} inet6 #{addr} alias") end def deladdr(addr) execute("ifconfig #{@name} inet6 #{addr} -alias") end def setmtu(mtu = 1500) execute("ifconfig #{@name} mtu #{mtu}") end def linklocal `ifconfig #{@name} inet6`.each { |s| if s =~ /inet6 (fe80::[^ ]*)/ return $1 end } return nil end def hostid laddr = linklocal if !laddr return nil end return laddr.sub(/^fe80::/, '').sub(/%.*$/, '') end private def initialize(name) @name = name end end class ClonedInterface < Interface def create @name = @tunif if @cloning cmd = sprintf("ifconfig %s create", @name) debugmsg("#{cmd}\n") `#{cmd}`.each { |l| if l =~ /^(#{@name}[0-9]+)/ @name = $1 break end } end @created = true end def created? return @created end def delete if @cloning && !@create_only execute(sprintf("ifconfig %s destroy", @name)) end @created = false end private def initialize(tunif, cloning, create_only) @tunif = tunif @cloning = cloning @create_only = create_only @created = false end end class GenericTunnel < ClonedInterface def create(me, her) super() execute(sprintf(TUNNEL_CREATE, @name, me, her)) up end def delete down execute(sprintf(TUNNEL_DELETE, @name)) super end def addpeer(me, her) execute("ifconfig #{@name} inet6 #{me} #{her} prefixlen 128 alias") end def delpeer(me, her) execute("ifconfig #{@name} inet6 #{me} #{her} prefixlen 128 -alias") end def setmtu(mtu = 1280) super(mtu) end end class NetgraphInterface < Interface def create if !@tunif || @tunif == "ng" @name = mkpeer @created = true return end if @tunif !~ /^ng([0-9]+)$/ raise "#{@tunif}: wrong name" end unitmax = $1.to_i shutdown(@tunif) bogus = Array.new while TRUE @name = mkpeer if @name == @tunif @created = true break end bogus.push(@name) if @name !~ /^ng([0-9]+)$/ raise "#{@name}: wrong name" end unit = $1.to_i if unit > unitmax raise "#{@name}: not expected" end end bogus.each { |iface| shutdown(iface) } end def created? return @created end def delete shutdown(@name) @created = false end private def initialize(tunif = nil) @tunif = tunif @created = false end def mkpeer() iface = nil f = IO.popen("ngctl -f - 2> /dev/null", "w+") f.write("mkpeer iface dummy inet6\n") f.write("msg dummy nodeinfo\n") f.write("quit\n") f.flush f.each_line { |line| if line =~ / name="(ng[0-9]+)"/ iface = $1 break end } f.close if !iface raise "ngctl failed" end return iface end def shutdown(iface = nil) if !iface iface = @name end execute("ngctl shutdown #{iface}: >/dev/null 2>&1") end end class UDPTunnel < NetgraphInterface def create(me, her) me_addr, me_port = me.split(';') her_addr, her_port = her.split(';') super() if !ksocket raise "ngctl: mkpeer ksocket fail" end if !bind(me_addr, me_port) raise "ngctl: bind fail addr=#{me_addr} port=#{me_port}" end if !connect(her_addr, her_port) raise "ngctl: connect fail addr=#{her_addr} port=#{her_port}" end up end def delete down super end def addpeer(me, her) execute("ifconfig #{@name} inet6 #{me} #{her} prefixlen 128 alias") end def delpeer(me, her) execute("ifconfig #{@name} inet6 #{me} #{her} prefixlen 128 -alias") end private def ksocket() execute("ngctl mkpeer #{@name}: ksocket inet6 inet/dgram/udp") end def bind(addr, port) execute("ngctl msg #{@name}:inet6 bind inet/#{addr}:#{port}") end def connect(addr, port) execute("ngctl msg #{@name}:inet6 connect inet/#{addr}:#{port}") end end class TunnelOnly attr_accessor :me attr_accessor :her attr_accessor :mtu def setup(intface) @intface = intface intface.create(@me, @her) if @mtu > 0 intface.setmtu(@mtu) end end def delete if @mtu > 0 @intface.setmtu end @intface.delete end private def initialize(me, her) @me = me @her = her end end class TunnelHost < TunnelOnly attr_accessor :me6 attr_accessor :her6 def setup(intface) super @intface.addpeer(@me6, @her6) end def delete @intface.delpeer(@me6, @her6) super end private def initialize(me, her, me6, her6) super(me, her) @me6 = me6 @her6 = her6 end end class TunnelNetwork < TunnelOnly attr_accessor :prefix attr_accessor :me6 attr_accessor :her6 def setup(intface) super if @me6 @intface.addpeer(@me6, @her6) end end def delete if @me6 @intface.delpeer(@me6, @her6) end super end private def initialize(me, her, prefix, me6 = nil, her6 = nil) super(me, her) @prefix = prefix @me6 = me6 @her6 = her6 end end def sendmsg(sock, msg) sock.print "#{msg}\r\n" debugmsg(">>#{msg}\n") end class DTCPClient def session connect_to(@dst, @port) { |sock, server| begin me = sock.addr()[3] logmsg("logging in to #{server[3]} port #{server[1]}\n") # get greeting begin t = sock.readline rescue return end debugmsg(">>#{t}") challenge = t.split(/ /)[1] #logmsg("authenticate(#{@username} #{challenge} #{@password}): ") response = authenticate(@username, challenge, @password) #logmsg("#{response\n") t = "tunnel #{@username} #{response} #{@tuntype}" if @mtu > 0 t.concat(" #{@mtu}") end if @udp_tunnel t.concat(" proto=udp") if !@behind_nat t.concat(" port=#{@udp_tunnel_port}") end end sendmsg(sock, t) begin t = sock.readline rescue return end debugmsg(">>#{t}") if (t !~ /^\+OK/) t.gsub!(/[\r\n]*$/, '') logmsg("failed, reason: #{t}") if (t =~ /^\-ERR authentication/) exit 1 end return end t.gsub!(/[\r\n]/, '') tun = t.split(/ /) if @behind_nat tun[1] = me elsif me != tun[1] logmsg("failed, you are behind a NAT box (#{me} != #{tun[1]})\n") exit 1 end if @udp_tunnel tun[1].concat(";#{@udp_tunnel_port}") end case tun.length when 3 @tunnel = TunnelOnly.new(tun[1], tun[2]) when 4 @tunnel = TunnelNetwork.new(tun[1], tun[2], tun[3]) when 5 @tunnel = TunnelHost.new(tun[1], tun[2], tun[3], tun[4]) when 6 @tunnel = TunnelNetwork.new(tun[1], tun[2], tun[5], tun[3], tun[4]) else return end @tunnel.mtu = @mtu # hook after session is established begin yield(@tunnel) rescue end keep_alive(sock) ensure begin sendmsg(sock, "quit") rescue end end } end def cleanup() # hook for cleanup begin yield(@tunnel) rescue end @tunnel = nil end private def initialize(dst, port, username, password, tuntype, behind_nat, mtu = 0, udp_tunnel = false, udp_tunnel_port = 0) @dst = dst @port = port @username = username @password = password @tuntype = tuntype @mtu = mtu @udp_tunnel = udp_tunnel @udp_tunnel_port = udp_tunnel_port @behind_nat = behind_nat @tunnel = nil end def connect_to(dst, port) res = [] begin res = Socket.getaddrinfo(dst, port, Socket::PF_INET, Socket::SOCK_STREAM, nil) rescue logmsg("FATAL: getaddrinfo failed (dst=#{dst} port=#{port})\n") return end if (res.size <= 0) logmsg("FATAL: getaddrinfo failed (dst=#{dst} port=#{port})\n") return end sock = nil begin server = [] res.each do |i| begin sock = TCPsocket.open(i[3], i[1]) rescue next end server = i break end if server == [] logmsg("could not connect to #{dst} port #{port}\n") return end yield(sock, server) ensure if sock begin sock.shutdown(1) rescue end begin sock.close rescue end end end end def keep_alive(sock) begin while TRUE debugmsg("sleep(60)\n") sleep 60 sendmsg(sock, "ping") t = select([sock], [], [sock], TIMEOUT) if t == nil break end if sock.eof? break end response = sock.readline debugmsg(">>#{response}") end rescue end end end #------------------------------------------------------------ port = 20200 username = `whoami`.chomp ousername = username password = '' tunif = TUNIF cloning = TUNIF_CLONING tuntype = 'tunnelonly' route_type = 'static' static_routes = 'default' tunif_addrs = '' prefix_delegation = '' $debug = DEBUG mtu = 0 udp_tunnel_port = UDP_TUNNEL_PORT behind_nat = false pidfile = PIDFILE # # test pattern # challenge = '0B1517C87D516A5FA65BED722D51A04F' # response = authenticate('foo', challenge, 'bar') # if response == 'DAC487C8DFBBF9EE5C7F8CDCC37B62A3' # logmsg("good!\n") # else # logmsg("something bad in authenticate()\n") # end # exit 0 if !getopts('acdDlnoU', 'A:', 'b:', 'f:', 'i:', 'm:', 'p:', 'P:', 'r:', 'R:', 't:', 'u:') usage() exit 0 end if ARGV.length != 1 usage() exit 1 end rtadvd_disable = $OPT_a tunif_addrs = $OPT_A if $OPT_A udp_tunnel_port = $OPT_b if $OPT_b cloning = false if $OPT_c $debug = $OPT_d daemonize = $OPT_D pidfile = $OPT_f if $OPT_f tunif = $OPT_i if $OPT_i loop = $OPT_l mtu = $OPT_m.to_i if $OPT_m behind_nat = $OPT_n create_only = $OPT_o port = $OPT_p if $OPT_p prefix_delegation = $OPT_P if $OPT_P route_type = $OPT_r if $OPT_r static_routes = $OPT_R if $OPT_R tuntype = $OPT_t if $OPT_t username = $OPT_u if $OPT_u udp_tunnel = $OPT_U dst = ARGV[0] if udp_tunnel && tunif !~ /^ng([0-9]+)?$/ tunif = "ng" end trap("SIGTERM", "EXIT") trap("SIGINT", "EXIT") trap("SIGHUP", "SIG_IGN") $syslog = Syslog.instance if daemonize daemon(0, 0) $syslog.open(File.basename($0), Syslog::LOG_PID, Syslog::LOG_DAEMON) logmsg("start") open(pidfile, 'w') { |p| p.print "#{$$}\n" } end begin password = getpassword(dst, username) pd = nil begin if udp_tunnel intface = UDPTunnel.new(tunif) else intface = GenericTunnel.new(tunif, cloning, create_only) end route = Route.new(route_type, static_routes) pd = PrefixDelegation.new(prefix_delegation.split(/\s*,\s*/), rtadvd_disable) dtcpc = DTCPClient.new(dst, port, username, password, tuntype, behind_nat, mtu, udp_tunnel, udp_tunnel_port) while TRUE interrupt = nil begin trap("SIGHUP") { raise(Interrupt, "SIGHUP") } dtcpc.session { |tunnel| tunnel.setup(intface) logmsg("tunnel to #{tunnel.her} established.\n") route.setup(intface) logmsg("default route was configured.\n") tunif_addrs.split(/\s*,\s*/).each { |addr| intface.addaddr(addr) } # prefix delegation if tunnel.instance_of?(TunnelNetwork) pd.update(tunnel.prefix.split(/\s*,\s*/)) end } rescue Interrupt => e if e.to_str != "SIGHUP" raise e end interrupt = e ensure trap("SIGHUP", "SIG_IGN") dtcpc.cleanup { |tunnel| if intface.created? tunif_addrs.split(/\s*,\s*/).each { |addr| intface.deladdr(addr) } route.delete tunnel.delete end } end unless loop break end if interrupt logmsg("restart by SIGHUP.\n") else logmsg("connection was lost.\n") sleep(10) end end ensure if pd pd.cleanup end end ensure if daemonize begin pid = -1 open(pidfile, 'r') { |p| pid = p.readline.to_i } if pid == $$ File.unlink(pidfile) end rescue end logmsg("exit\n") end end exit 0