#!/usr/pkg/bin/ruby32

#
# dtcpc, Turmpet Dynamic Tunnel Configuration Protocol client
#

#
# Copyright (c) 2000-2013 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.75 2013/06/02 14:05:51 ume Exp $
#

require 'optparse'
require "socket"
require "digest/md5"
require 'syslog'

TIMEOUT = 60
DEBUG = false
PASSWDFILE = '/usr/pkg/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 = 'gif0'
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_line { |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 = Digest::MD5.new
  m.update(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_line { |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_line { |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

begin
  params = ARGV.getopts('aA:b:cdDf:i:lm:nop:P:r:R:t:u:U')
rescue
  usage()
  exit 0
end
if ARGV.length != 1
  usage()
  exit 1
end
rtadvd_disable = params["a"]
tunif_addrs = params["A"] if params["A"]
udp_tunnel_port = params["b"].to_i if params["b"]
cloning = false if params["c"]
$debug = params["d"]
daemonize = params["D"]
pidfile = params["f"] if params["f"]
tunif = params["i"] if params["i"]
loop = params["l"]
mtu = params["m"].to_i if params["m"]
behind_nat = params["n"]
create_only = params["o"]
port = params["p"].to_i if params["p"]
prefix_delegation = params["P"] if params["P"]
route_type = params["r"] if params["r"]
static_routes = params["R"] if params["R"]
tuntype = params["t"] if params["t"]
username = params["u"] if params["u"]
udp_tunnel = params["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
