#! /usr/local/bin/ruby

#
# dtcpc, Turmpet Dynamic Tunel Configuration Protocol client
#

#
# Copyright (c) 2000-2002 Hajimu UMEMOTO <ume@mahoroba.org>
# 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.51 2002/11/28 11:07:54 ume Exp $
#

require 'getopts'
require "socket"
require "md5"
require 'syslog'

# XXX should be derived from system headers
IPPROTO_IPV6 = 41
IPV6_FAITH = 29
TIMEOUT = 60
TUNTIMEOUT = 300
DEBUG = false
PASSWDFILE = '/usr/local/etc/dtcpc.auth'
PIDFILE = '/var/run/dtcpc.pid'

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

def usage()
  $stderr.print "usage: #{File.basename($0)} [-cdDln] [-i if] [-p port] [-t tuntype] [-u username] [-A addr] [-R dest] [-P prefix-delegation] server\n"
end

class PDInfo
  attr_accessor :slaid
  attr_accessor :hostid
  private
  def initialize(slaid, hostid)
    @slaid = slaid
    @hostid = hostid
  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 #{@pdinfo.keys.join(' ')}")
	@rtadvd_running = true
	@rtadvd_invoked = true
      end
    end
  end

  def finish(*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 = pdinfo.split('/')
      @pdinfo[iface] = PDInfo.new(slaid, hostid)
    }
    if @pdinfo.size <= 0
      return
    end
    @forwarding_enabled = false
    @rtadvd_invoked = false
    @rtadvd_running = rtadvd_running?
    @rtadvd_disable = rtadvd_disable[0]
  end

  def rtadvd_running?
    `ps ax`.each { |s|
      if s.scan(/rtadvd/).size > 0
	return true
      end
    }
    return false
  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 { |iface, pdinfo|
      addr = to_addr(nla_ary, pdinfo.slaid, pdinfo.hostid)
      execute("ifconfig #{iface} inet6 #{addr} alias")
    }
  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 { |iface, pdinfo|
      addr = to_addr(nla_ary, pdinfo.slaid, pdinfo.hostid)
      execute("ifconfig #{iface} inet6 #{addr} -alias")
    }
  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 seed()
  m = MD5.new(Time.now.to_s)
  m.update($$.to_s)
  m.update(Socket.gethostname())
  return m.digest.unpack("H32")[0].tr('a-f', 'A-F')
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(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 getladdr(tunif)
  `ifconfig #{tunif} inet6`.each { |s|
    laddr = s.scan('inet6 (fe80::[^ ]*)')[0]
    if laddr
      return laddr
    end
  }
  return nil
end

def route_add(dest, tunif)
  case ROUTE_METHOD
  when ROUTE_CHANGE
    cmd = "route add -inet6 #{dest} ::1; route change -inet6 #{dest} -ifp #{tunif}"
  when ROUTE_INTERFACE
    cmd = "route add -inet6 #{dest} -interface #{tunif}"
  when ROUTE_IFP
    cmd = "route add -inet6 #{dest} ::1 -ifp #{tunif}"
  else
    laddr = getladdr(tunif)
    if !laddr
      logmsg("FATAL: cannot get link-local address of #{tunif}\n")
      begin
	$sock.shutdown(1)
      rescue
      end
      exit 1
    end
    cmd = "route add -inet6 #{dest} #{laddr}"
  end
  return cmd
end

def cleanup()
  if $sock
    begin
      $sock.print "quit\r\n"
    rescue
    end
    begin
      $sock.shutdown(1)
    rescue
    end
    begin
      $sock.close
    rescue
    end
  end
  if $tuninfo.length == 5
    execute("ifconfig #{$intface} inet6 #{$tuninfo[3]} #{$tuninfo[4]} -alias")
  else
    $tunif_addrs.split('\s*,\s*').each { |tunif_addr|
      execute("ifconfig #{$intface} inet6 #{tunif_addr} -alias")
    }
  end
  execute("ifconfig #{$intface} down")
  if $route == "static"
    $static_routes.split('\s*,\s*').each { |static_route|
      execute("route delete -inet6 #{static_route}")
    }
  end
  execute(sprintf(TUNNEL_DELETE, $intface))
  if $cloning
    execute(sprintf("ifconfig %s destroy", $intface))
  end
end

def dtcpc(dst, port, username, password, tuntype)
  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

  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

  me = $sock.addr()[3]

  logmsg("logging in to #{server[3]} port #{server[1]}\n")
  # get greeting
  begin
    t = $sock.readline
  rescue
    begin
      $sock.shutdown(1)
      $sock.close
    rescue
    end
    return
  end
  debugmsg(">>#{t}")
  challenge = t.split(/ /)[1]

  #logmsg("authenticate(#{username} #{challenge} #{password}): ")
  response = authenticate(username, challenge, password)
  #logmsg("#{response\n")
  $sock.print "tunnel #{username} #{response} #{tuntype}\r\n"
  debugmsg(">>tunnel #{username} #{response} #{tuntype}\n")

  begin
    t = $sock.readline
  rescue
    begin
      $sock.shutdown(1)
      $sock.close
    rescue
    end
    return
  end
  $tuninfo = []
  debugmsg(">>#{t}")
  if (t !~ /^\+OK/)
    t.gsub!(/[\r\n]*$/, '')
    logmsg("failed, reason: #{t}")
    $sock.print "quit\r\n"
    begin
      $sock.shutdown(1)
      $sock.close
    rescue
    end
    if (t =~ /^\-ERR authentication/)
      if $daemonize
	File.unlink($pidfile)
      end
      exit 1
    end
    return
  end

  t.gsub!(/[\r\n]/, '')
  $tuninfo = t.split(/ /)
  if me != $tuninfo[1] && !$nat
    logmsg("failed, you are behind a NAT box (#{me} != #{$tuninfo[1]})\n")
    $sock.print "quit\r\n"
    begin
      $sock.shutdown(1)
      $sock.close
    rescue
    end
    if $daemonize
      File.unlink($pidfile)
    end
    exit 1
  end
  if $cloning
    cmd = sprintf("ifconfig %s create", $intface)
    debugmsg("#{cmd}\n")
    `#{cmd}`.each { |l|
      tunif = l.scan("^(#{$intface}[0-9]+)")[0]
      if tunif
	$intface = tunif
	break
      end
    }
  end
  execute(sprintf(TUNNEL_CREATE, $intface, me, $tuninfo[2]))
  # global address for the tunnel is given
  if $tuninfo.length == 5
    execute("ifconfig #{$intface} inet6 #{$tuninfo[3]} #{$tuninfo[4]} prefixlen 128 alias")
  end
  execute("ifconfig #{$intface} up")
  if $tuninfo.length != 5
    $tunif_addrs.split('\s*,\s*').each { |tunif_addr|
      execute("ifconfig #{$intface} inet6 #{tunif_addr} alias")
    }
  end
  logmsg("tunnel to #{$tuninfo[2]} established.\n")
  if $route == "static"
    $static_routes.split('\s*,\s*').each { |static_route|
      execute("route delete -inet6 #{static_route} > /dev/null 2>&1")
      execute(route_add(static_route, $intface))
    }
  end
  if $route == "solicit"
    execute("rtsol #{$intface}")
  end
  logmsg("default route was configured.\n")

  # prefix delegation
  if $tuninfo.length == 4
    $pd.update($tuninfo[3].split('\s*,\s*'))
  end

  begin
    while TRUE
      debugmsg("sleep(60)\n")
      sleep 60
      $sock.print "ping\r\n"
      debugmsg(">>ping\n")
      t = select([$sock], [], [$sock], TIMEOUT)
      if t == nil
	break
      end
      if $sock.eof?
	break
      end
      response = $sock.readline
      debugmsg(">>#{response}")
    end
  rescue
  end
  cleanup()
end

#------------------------------------------------------------

port = 20200
username = `whoami`.chomp
ousername = username
password = ''
$intface = TUNIF
$cloning = TUNIF_CLONING
tuntype = 'tunnelonly'
$route = 'static'
$static_routes = 'default'
$tunif_addrs = ''
$prefix_delegation = ''
$debug = DEBUG
$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('acdDln', 'A:', 'f:', 'i:', '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
$cloning = false if $OPT_c
$debug = $OPT_d
$daemonize = $OPT_D
$pidfile = $OPT_f if $OPT_f
$intface = $OPT_i if $OPT_i
loop = $OPT_l
$nat = $OPT_n
port = $OPT_p if $OPT_p
$prefix_delegation = $OPT_P if $OPT_P
$route = $OPT_r if $OPT_r
$static_routes = $OPT_R if $OPT_R
tuntype = $OPT_t if $OPT_t
username = $OPT_u if $OPT_u
dst = ARGV[0]

$pd = PrefixDelegation.new($prefix_delegation.split('\s*,\s*'), rtadvd_disable)
$syslog = Syslog.instance

if $daemonize
  daemon(0, 0)
  $syslog.open(File.basename($0), Syslog::LOG_PID, Syslog::LOG_DAEMON)
  logmsg("start")
  open($pidfile, "w") { |pid|
    pid.print "#{$$}\n"
  }
end

password = getpassword(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
password.chomp!()

$sock = nil

def bye()
  cleanup()
  if $daemonize
    File.unlink($pidfile)
  end
  $pd.finish
  exit 0
end

trap("SIGTERM") {
  if $daemonize
    logmsg("exit with SIGTERM\n")
  end
  bye()
}
trap("SIGINT") {
  if $daemonize
    logmsg("exit with SIGINT\n")
  end
  bye()
}

while TRUE
  dtcpc(dst, port, username, password, tuntype)
  unless loop
    break
  end
  logmsg("connection was lost.\n")
  sleep(10)
end

if $daemonize
  File.unlink($pidfile)
  logmsg("exit\n")
end
exit 0
