#! /usr/local/bin/ruby

#
# dtcpd, Turmpet Dynamic Tunel Configuration Protocol daemon
#

#
# Copyright (c) 2000-2003 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: dtcps.rb,v 1.3 2000/04/21 14:21:21 jinmei Exp $
# $Mahoroba: src/dtcp/dtcps.rb,v 1.40 2003/01/01 19:46:43 ume Exp $
#

require 'getopts'
require "socket"
require "thread"
require "md5"
require "dbm"
require "etc"
require 'syslog'

# XXX should be derived from system headers
IPPROTO_IPV6 = 41
IPV6_FAITH = 29
TUNIF = "gif[0-9]+"
AUTHTIMEOUT = 60
TUNTIMEOUT = 300
# must be less than 10, against RIPng and PIM6 - useless
TRAFFICTIMEOUT = 0
POPAUTHUID = 'pop'
ROUTETABLE = '/usr/local/etc/routetable'
PIDFILE = '/var/run/dtcps.pid'

ROUTE_GATEWAY = 0
ROUTE_CHANGE = 1
ROUTE_INTERFACE = 2
ROUTE_IFP = 3

# FreeBSD port of qpopper 4.X
POPAUTHDB = '/usr/local/etc/qpopper/pop.auth'
# FreeBSD port of qpopper 2.X
#POPAUTHDB = '/usr/local/etc/popper/pop.auth'
# NetBSD pkg of qpopper 4.X
#POPAUTHDB = '/usr/pkg/etc/apop.auth'

# NetBSD 1.5.x or earlier
# 	and OpenBSD
#TUNNEL_CREATE = 'ifconfig %s tunnel %s %s'
#TUNNEL_DELETE = 'ifconfig %s deletetunnel'
#ROUTE_METHOD = ROUTE_CHANGE
#
# FreeBSD 4.6-RELEASE or later
# 	and NetBSD 1.6
# (We don't support gif cloning, yet. You must create gifs before)
TUNNEL_CREATE = 'ifconfig %s tunnel %s %s'
TUNNEL_DELETE = 'ifconfig %s deletetunnel'
ROUTE_METHOD = ROUTE_IFP
#
# FreeBSD 4.4-RELEASE and 4.5-RELEASE
# (We don't support gif cloning, yet. You must create gifs before)
#TUNNEL_CREATE = 'ifconfig %s tunnel %s %s'
#TUNNEL_DELETE = 'ifconfig %s deletetunnel'
#ROUTE_METHOD = ROUTE_GATEWAY
#
# FreeBSD 4.3-RELASE or earlyer
#TUNNEL_CREATE = 'gifconfig %s %s %s'
#TUNNEL_DELETE = 'gifconfig %s delete'
#ROUTE_METHOD = ROUTE_GATEWAY

class Tunnel

  attr :thread
  attr :tun

  private

  def initialize(tun)
    @thread = Thread.current
    @tun = tun
  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 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)
  debugmsg("#{cmd}\n")
  system(cmd)
end

def gettunnel(me, her)
  `ifconfig -a`.each { |s|
    next if s !~ /^#{$tunif}:/ || s =~ /UP/o
    s = s[0, s.index(':')]
    execute(sprintf(TUNNEL_CREATE, s, me, her))
    execute("ifconfig #{s} up")
    return s
  }
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
      return nil
    end
    cmd = "route add -inet6 #{dest} #{laddr}"
  end
  return cmd
end

def getladdr(tunif)
  `ifconfig #{tunif} inet6`.each { |s|
    laddr = s.scan('inet6 (fe80::[^ ]*)')[0]
    if laddr
      return laddr
    end
  }
  return nil
end

def routesetup(user, tunif)
  her_prefixes = nil
  begin
    open(ROUTETABLE) do |f|
      f.each_line do |l|
	l.chop!.sub!('\s*#.*$', '')
	next if l =~ /^\s*$/o
	if l =~ /^#{user}\s+(.*)$/
	  her_prefixes = $1
	  break
	end
      end
    end
  rescue
    debugmsg("routetable is not found\n")
  end
  if her_prefixes
    her_prefixes.split('\s*,\s*').each { |her_prefix|
      heraddr, herlen = her_prefix.split('/')
      cmd = route_add("#{heraddr} -prefixlen #{herlen}", tunif)
      if !cmd
	debugmsg("#{user}: cannot get link-local address of #{tunif}\n")
	return nil
      end
      execute(cmd)
    }
  else
    logmsg("#{user}: not registered to use tunnlroute\n")
  end
  return her_prefixes
end    

def tunnelsetup(s, user, type)
  me = s.addr()[3]
  her = s.peeraddr()[3]
  debugmsg("#{s}: tunnel #{me} -> #{her}\n")

  case type
  when 'host'
    if $prefix == nil
      debugmsg("#{s}: tunnel type #{type} not supported\n")
      return [], "unsupported tunnel type #{type}"
    end

    tunif = gettunnel(me, her)
    if (tunif == nil)
      debugmsg("#{s}: tunnel interface sold out\n")
      return [], 'tunnel interface sold out'
    end
    debugmsg("#{s}: tunnel interface #{tunif}\n")

    if tunif =~ /(\d+)$/
      tunid = $1.to_i
      heraddr = sprintf("%s%04x", $prefix, (tunid + 1) * 4 + 2)
      myaddr = sprintf("%s%04x", $prefix, (tunid + 1) * 4 + 1)
      execute("ifconfig #{tunif} inet6 #{myaddr} #{heraddr} prefixlen 128 alias")
      x = [tunif, her, $global ? $global : me, heraddr, myaddr]
      err = nil
    else
      tunnelcleanup([tunif, her, me])
      x = []
      err = 'internal error: tunnel interface name format is wrong'
    end
  when 'tunnelonly', 'network', 'tunnelroute'
    tunif = gettunnel(me, her)
    if (tunif == nil)
      debugmsg("#{s}: could not configure tunnel\n")
      return [], 'tunnel interface sold out'
    end
    debugmsg("#{s}: tunnel interface #{tunif}\n")
    if type == 'network' || type == 'tunnelroute'
      her_prefixes = routesetup(user, tunif)
      if !her_prefixes
	tunnelcleanup([tunif, her, me])
	x = []
	err = 'prefix is not assigned'
      else
	x = [tunif, her, $global ? $global : me, her_prefixes]
	err = nil
      end
    else
      x = [tunif, her, $global ? $global : me]
      err = nil
    end
  else
    debugmsg("#{s}: unsupported tunnel type #{type}\n")
    err = "unsupported tunnel type #{type}"
    x = []
  end
  return x, err
end

def getipkts(intface)
  tmpfile = "/tmp/getipkts#{$$}.#{intface}"
  system("netstat -in -I #{intface} > #{tmpfile}")
  f = open(tmpfile, "r")
  s = f.readline
  s = f.readline
  f.close
  File::unlink(tmpfile)
  t = s.split(/[ \t]+/)
  if t.length < 5
    debugmsg("#{intface} ipkts unknown, returning -1\n")
  end
  debugmsg("#{intface} ipkts = #{t[t.length - 5]}\n")
  return t[t.length - 5]
end

def checktraffic(tun)
  return if TRAFFICTIMEOUT == 0
  ipkts = getipkts(tun[0])
  while TRUE
    sleep TRAFFICTIMEOUT
    i = getipkts(tun[0])
    next if i == -1
    break if ipkts >= i
    ipkts = i
  end
end

def tunnelcleanup(tun)
  logmsg("#{tun[0]} disconnected\n")
  if tun.length == 5
    execute("ifconfig #{tun[0]} inet6 #{tun[4]} #{tun[3]} -alias")
  elsif tun.length == 4
    tun[3].split('\s*,\s*').each { |her_prefix|
      heraddr, herlen = her_prefix.split('/')
      execute("route delete -inet6 #{heraddr} -prefixlen #{herlen}")
    }
  end
  execute("ifconfig #{tun[0]} down")
  execute(sprintf(TUNNEL_DELETE, tun[0]))
end

def service_dtcp(sock, name)
  debugmsg("service_dtcp(#{sock}, #{name})\n")
  while TRUE
    debugmsg("service_dtcp(#{sock}, #{name}) accepting\n")
    Thread.start(sock.accept) { |s|
      debugmsg("service_dtcp(#{sock}, #{name}) accepted #{s}\n")
      tun = []
      user = nil

      # send challenge
      challenge = seed()
      s.print "+OK #{challenge} KAME tunnel server\r\n"

      # check response
      # tunnel itojun RESPONSE type
      while TRUE
	t = select([s], [], [s], tun == [] ? AUTHTIMEOUT : TUNTIMEOUT)
	if t == nil
	  s.print "-ERR connection timed out, disconnecting\r\n"
	  break
	end
	if s.eof?
	  break
	end
	if user
	  # be careful.  it may accesses non existence member wrongly.
	  # so, make sure to copy context, 1st.
	  alive = $tunnel[user]
	  if !alive
	    debugmsg("#{user} was disconnected\n")
	    break
	  end
	  if alive.thread != Thread.current
	    debugmsg("#{user} has another new session\n")
	    break
	  end
	end
	response = s.readline
	response.gsub!(/[\n\r]/, '')
	if response != ''
	  t = response.split(/ /)
	  t[0].tr!('A-Z', 'a-z')
	else
	  t = ['']
	end
	debugmsg("#{s}: got <#{response}>\n")
	case t[0]
	when 'tunnel'
	  if (t.length != 4)
	    logmsg("client #{s} sent wrong #{t[0]} command\n")
	    debugmsg("#{s}: sent <-ERR authentication failed.>\n")
	    s.print "-ERR authentication failed.\r\n"
	    next
	  end
	  user = t[1]
	  type = t[3]
	  pass = getpopauth(user)
	  if pass == nil
	    logmsg("client #{s} has no password in database for #{user}\n")
	    debugmsg("#{s}: sent <-ERR authentication failed.>\n")
	    s.print "-ERR authentication failed.\r\n"
	    next
	  end
	  # get password from the username
#	  $stderr.print "authenticate(#{user} #{challenge} #{pass}): "
#	  debugmsg(authenticate(user, challenge, pass) + "\n")
#	  debugmsg("target: #{t[2]}\n")
 	  if (authenticate(user, challenge, pass) == t[2])
	    debugmsg("client #{s.peeraddr()[3]} on #{s}\n")
	    logmsg("client #{s.peeraddr()[3]} authenticated as #{user}\n")
	    auth = true
	    err = ''
	    $mutex.synchronize {
	      if $tunnel.has_key?(user)
		logmsg("#{user}: duplicate login was detected\n")
		tunnelcleanup($tunnel[user].tun)
		$tunnel.delete(user)
	      end
	      her = s.peeraddr()[3]
	      $tunnel.each { |u, t|
		if t.tun[1] == her
		  logmsg("#{user}: her IPv4 address #{her} was conflicted with #{u}\n")
		  tunnelcleanup(t.tun)
		  $tunnel.delete(u)
		  break
		end
	      }
	      tun, err = tunnelsetup(s, user, type)
	      if tun != []
		$tunnel[user] = Tunnel.new(tun)
	      end
	    }
	    if tun == []
	      logmsg("failed to configure for #{user} type #{type}: #{err}\n")
	      debugmsg("#{s}: sent <-ERR #{err}>\n")
	      s.print "-ERR #{err}\r\n"
	    else
	      t = tun[1, tun.length - 1]
	      logmsg("#{tun[0]} configured for #{user} type #{type}: " + t.join(' ') + "\n")
	      debugmsg("#{s}: sent <+OK #{t.join(' ')}>\n")
	      s.print "+OK ", t.join(' '), "\r\n"
	    end
	  else
	    logmsg("client #{s} not authenticated\n")
	    debugmsg("#{s}: sent <-ERR authentication failed.>\n")
	    s.print "-ERR authentication failed.\r\n"
	  end
	when 'ping'
	  debugmsg("#{s}: sent <+OK hi, happy to hear from you>\n")
	  s.print "+OK hi, happy to hear from you\r\n"
	when 'help'
	  debugmsg("#{s}: sent <+OK valid commands are: TUNNEL PING QUIT>\n")
	  s.print "+OK valid commands are: TUNNEL PING QUIT\r\n"
	when 'quit'
	  debugmsg("#{s}: sent <+OK see you soon.>\n")
	  s.print "+OK see you soon.\r\n"
	  break
	else
	  debugmsg("client #{s} sent invalid command #{t[0]}\n")
	  debugmsg("#{s}: sent <-ERR invalid command>\n")
	  s.print "-ERR invalid command\r\n"
	end
      end
      if tun != []
	$mutex.synchronize {
	  if $tunnel.has_key?(user) && $tunnel[user].thread == Thread.current
	    checktraffic(tun)
	    tunnelcleanup(tun)
	    $tunnel.delete(user)
	  end
	}
      end
      s.flush
      s.shutdown(1)
      debugmsg("shutdown #{s} #{Thread.current}\n")
    }
  end
  debugmsg("service_dtcp(#{sock}, #{name}) finished\n")
end

def usage()
  $stderr.print "usage: #{File.basename($0)} [-dD] [-i interfaces] [-p port] [prefix]\n"
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

# NOTE: strings are terminated by "\0"...
def getpopauth(user)
  pw = Etc.getpwnam(POPAUTHUID)
  if pw == nil
    debugmsg("no user named pop\n")
    return nil
  end
  origuid = Process.euid
  # XXX begin seteuid(pop)
  Process.euid = pw[2]
  f = DBM.open(POPAUTHDB, nil)
  if f == nil
    debugmsg("no password database found\n")
    Process.euid = origuid
    return nil
  end
  p = f[user + "\0"]
  f.close
  Process.euid = origuid
  # XXX end seteuid(pop)
  if p == nil
    debugmsg("no relevant password database item found\n")
    return nil
  end
  while p.length > 0 && p[p.length - 1] == 0
    p = p[0, p.length - 1]
  end
  for i in 0 .. p.length - 1
    p[i] = [p[i] ^ 0xff].pack('C')
  end
  debugmsg("ok, relevant password database item found\n")
  return p
end

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

port = 20200
$tunif = TUNIF
$global = nil
$prefix = nil

if !getopts('dD', 'g:', 'i:', 'p:')
  usage()
  exit 0
end
$debug = $OPT_d
$daemonize = !$OPT_D
$global = $OPT_g if $OPT_g
$tunif = $OPT_i if $OPT_i
port = $OPT_p if $OPT_p

case ARGV.length
when 0
  $prefix = nil
when 1
  $prefix = ARGV[0]
  if $prefix !~ /^[0-9a-fA-f:]*::$/
    usage()
    exit 1
  end
else
  usage()
  exit 1
end

res = []
t = Socket.getaddrinfo(nil, port, Socket::PF_INET, Socket::SOCK_STREAM,
      nil, Socket::AI_PASSIVE)
if (t.size <= 0)
  logmsg("FATAL: getaddrinfo failed (port=#{port})\n")
  exit 1
end
res += t

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

$mutex = Mutex.new
$tunnel = Hash.new

sockpool = []
names = []
listenthreads = []

res.each do |i|
  s = TCPserver.new(i[3], i[1])
  n = Socket.getnameinfo(s.getsockname, Socket::NI_NUMERICHOST|Socket::NI_NUMERICSERV).join(" port ")
  s.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
  sockpool.push s
  names.push n
end

if $debug
  (0 .. sockpool.size - 1).each do |i|
    debugmsg("listen[#{i}]: #{sockpool[i]} #{names[i]}\n")
  end
end

trap("SIGTERM") {
  debugmsg("SIGTERM was received\n")
  $mutex.synchronize {
    $tunnel.each_key { |user|
      tunnelcleanup($tunnel[user].tun)
      $tunnel.delete(user)
    }
  }
  # for safety
  `ifconfig -lu`.chop.split(/ +/o).grep(/^#{$tunif}$/).each { |i|
    execute("ifconfig #{i} down")
    execute(sprintf(TUNNEL_DELETE, i))
  }
  if $daemonize
    File.unlink(PIDFILE)
  end
  exit 0
}

(0 .. sockpool.size - 1).each do |i|
  listenthreads[i] = Thread.start {
    debugmsg("listen[#{i}]: thread #{Thread.current}\n")
    service_dtcp(sockpool[i], names[i])
  }
end

for i in listenthreads
  if VERSION =~ /^1\.2/
    Thread.join(i)
  else
    i.join
  end
end

if $daemonize
  File.unlink(PIDFILE)
end
exit 0
