#!/usr/bin/ruby

require 'time'

class ClickGenerator

  def initialize
    @label = Hash.new
  end

  def printConfig(node=nil) 

    dir = "#{$outdir}/#{node.slice}@#{node.dnsname}"
    system("mkdir -p #{dir}")
    outfile = "#{dir}/click.cfg"
    @f = open(outfile.to_s, "w+")

    @click_forwarding = false
    if node.click_forwarding 
#          if node.router == "XORP"
        @click_forwarding = true
#      else
 #       print "\n*** Click forwarding only used with XORP, ignoring ***\n"
 #     end
    end

    printHeader(node)
    printAddresses(node)
    printNAPTElements
    printMacTable(node)
    printIPForwarding(node)
    printMacOutputs(node)
    printIPOutputs(node)
    printOpenVPNOutput(node)

    @f.close
  end

  def printHeader(node)
    ctime = Time.new
   
    @f.print <<EOF
// Click configuration for #{node.dnsname}
// automatically generated at #{ctime}
    
Message("Click started on: #{node.dnsname} @ config time: #{ctime}");
EOF
  end
  
  def printAddresses(node)

    lbl = node.label
    @f.print <<EOF

// Address information for this node and its immediate neighbors
AddressInfo (
  #{lbl}-eth0         #{node.iface[0].ipaddr}/32  #{node.iface[0].macaddr},
  #{lbl}-real         #{node.realip},
  #{lbl}-tap0         #{node.tap0.ipaddr}  #{node.tap0.ipaddr}/32  #{node.tap0.macaddr},
  #{lbl}-fea          #{node.fea.ipaddr} #{node.fea.macaddr},
  egress-local      #{node.iface[1].ipaddr}/32  #{node.iface[1].macaddr},
  egress-nexthop    #{node.nat_gateway.ipaddr} #{node.nat_gateway.ipaddr}/32 #{node.nat_gateway.macaddr},
  clientnet         #{node.clientnet.ipaddr}/24 #{node.clientnet.macaddr},
EOF

    node.iface.each { |i|
      if (i.neighbor)
        nbrnode = i.neighbor.node
        nbr = nbrnode.label
        @f.print <<EOF

  #{nbr}-tap0         #{nbrnode.tap0.macaddr},
  #{nbr}-real         #{nbrnode.realip},
  #{nbr}-eth0         #{nbrnode.iface[0].macaddr},
  #{nbr}-#{lbl}         #{i.neighbor.ipaddr}/32  #{i.neighbor.macaddr},
  #{lbl}-#{nbr}         #{i.ipaddr}/32  #{i.macaddr},
  #{nbr}-fea          #{nbrnode.fea.ipaddr}  #{nbrnode.fea.macaddr},
EOF
      end
    }
    @f.print ");\n"
  end

  def printMacTable(node)
    @f.print <<EOF
    
mac_table :: {
  input -> Strip(14) -> ToDump(/home/#{node.slice}/mon/data/macin.dat, SNAPLEN 28, ENCAP IP) -> Unstrip(14) 
-> dst_mac :: Classifier(
    0/#{node.fea.macaddr.gsub(':','')},
EOF
    nextout = 0
    node.iface.each { |i|
      @f.print "    0/#{i.macaddr.gsub(':','')},\n"
    }
    @f.print "    0/#{node.clientnet.macaddr.gsub(':','')},\n"
    @f.print "    -);\n"

    @fea_output = nextout
    @f.print "  dst_mac[0] -> [#{@fea_output}]output;  // To IP FEA\n"

    @uml_output = (nextout += 1)
    num = 1
    node.iface.each {
      @f.print "  dst_mac[#{num}] -> [#{@uml_output}]output;  // To UML\n"
      num += 1
    }
    
    @client_output = (nextout += 1)
    @f.print "  dst_mac[#{num}] -> [#{@client_output}]output;  // To OpenVPN clients\n"
    num += 1

    @f.print "  dst_mac[#{num}] -> src_mac :: Classifier(\n"
    node.iface.each { |i|
      @f.print "    6/#{i.macaddr.gsub(':','')},\n"
    }
    node.iface.each { |i|
      if i.neighbor
        @f.print "    6/#{i.neighbor.macaddr.gsub(':','')},\n"
      end
    }

    @tap0_output = (nextout += 1)
    @napt_output = (nextout += 1)
    
    @f.print <<EOF
    -);
  src_mac[0] -> [#{@tap0_output}]output;  // To tap
  src_mac[1] -> [#{@napt_output}]output;  // To NAPT
EOF

    @iface_output = (nextout += 1)
    num = 2
    node.iface.each { |i|
      if i.neighbor
        @f.print "  src_mac[#{num}] -> [#{nextout}]output;  // To tunnel\n"
        num += 1
        nextout += 1
      end
    }
    @ifacerw_output = nextout
    node.iface.each { |i|
      if i.neighbor
        @f.print "  src_mac[#{num}] -> [#{nextout}]output;  // Write " +
          "dst mac to UML\n"
        num += 1
        nextout += 1
      end
    }

    @f.print <<EOF
  src_mac[#{num}] -> Discard;
}; // mac_table
EOF
  end

  def printMacOutputs(node)

      @f.print <<EOF

// Packets to/from UML
// Respond to ARP queries on eth0, eth1
uml :: Queue -> UMLSwitch 
-> Strip(14) -> ToDump(/home/#{node.slice}/mon/data/umlout.dat, SNAPLEN 28, ENCAP IP) -> Unstrip(14)
-> ethtype :: Classifier(
      6/#{node.iface[0].macaddr.gsub(':','')} 12/0806 20/0001, 
      6/#{node.iface[1].macaddr.gsub(':','')} 12/0806 20/0001, 
EOF
    if @click_forwarding
      @f.print "      12/0800 !30/e0,    // IP packets to FEA \n"
    end
    @f.print <<EOF
      -)
ar :: ARPResponder(#{node.label}-tap0, egress-nexthop, clientnet)
-> uml
ethtype[0] -> ar
ethtype[1] -> ar
EOF
    if @click_forwarding
      @f.print <<EOF
ethtype[2] -> Strip(14) -> CheckIPHeader -> ToDump(/home/#{node.slice}/mon/data/feain.dat, SNAPLEN 28, ENCAP IP) -> ip_fea
ethtype[3] -> mac_table 
EOF
    else
      @f.print "ethtype[2] -> mac_table\n"
    end

    @f.print <<EOF

// Packets from a tunnel
Socket( UDP, 0.0.0.0, #{$udp_port} ) -> mac_table

EOF
    if @click_forwarding
      @f.print <<EOF
// Packets from tap0 go to the IP forwarding engine               
FromHost(tap0)
-> Strip(14)
-> ToDump(/home/#{node.slice}/mon/data/feain.dat, SNAPLEN 28, ENCAP IP) -> ip_fea
EOF
    else
      @f.print <<EOF
// Packets from tap0                
FromHost(tap0)
-> StoreData( 0, "#{'\x' + node.iface[0].macaddr.gsub(':','\x')}" )
-> mac_table
EOF
    end
      
    if @click_forwarding
      @f.print <<EOF
egress :: Strip(14)
-> egress_ip :: NaptEgress(#{node.label}-real)
-> ip_fea
EOF
    else
      @f.print <<EOF
// Packets to/from the egress NAPT
egress :: Strip(14)
-> NaptEgress(#{node.label}-real)
-> EtherEncap(0x0800, 0:0:0:0:0:0, egress-local)
-> mac_table
EOF
    end

    @f.print <<EOF

mac_table[#{@uml_output}] -> uml

tap0 :: ToHost(tap0)
mac_table[#{@tap0_output}] -> tap0 

mac_table[#{@napt_output}] -> egress

EOF
    out = @iface_output
    node.iface.each_index { |idx|
      i = node.iface[idx]
      if i.neighbor
        nbr = i.neighbor.node.label
        @f.print <<EOF
tunnel#{idx} :: RandomSample(DROP #{i.link.loss})
-> Socket( UDP, #{nbr}-real, #{$udp_port}, CLIENT true );
mac_table[#{out}] -> tunnel#{idx}

EOF
        out += 1
      end
    }

    idx = @ifacerw_output
    @f.print "//Overwrite MAC destination\n"
    node.iface.each { |i|
      if i.neighbor
        @f.print <<EOF
mac_table[#{idx}] -> StoreData( 0,"#{'\x' + i.macaddr.gsub(':','\x')}" ) -> uml
EOF
        idx += 1
      end
    }
  end

  def printIPForwarding(node) 

    return if ! @click_forwarding

    num = node.iface.size
    lbl = node.label
    @f.print <<EOF

// Click IP forwarding engine (populated by XORP)
_xorp_rt4 :: RadixIPLookup(
  #{lbl}-eth0 #{num},
  #{lbl}-tap0 #{num += 1},
  egress-local #{num += 1},
EOF

    node.iface.each { |i|
      if i.neighbor
        @f.print "  #{lbl}-#{i.neighbor.node.label} #{num += 1},\n"
      end
    }
   
    @f.print <<EOF
);

ip_fea :: DecIPTTL -> ToDump(/home/#{node.slice}/mon/data/feaout.dat, SNAPLEN 28, ENCAP IP) -> _xorp_rt4;
ip_fea[1] -> ICMPError(#{lbl}-fea, timeexceeded) -> _xorp_rt4;

EOF
  end

  def printIPOutputs(node)

    if (! @click_forwarding)
      @f.print <<EOF

// FEA output disabled (no Click FEA forwarding for this node)
mac_table[#{@fea_output}] -> Discard
EOF
      return
    end

    num = 0
    lbl = node.label
    fea = "#{lbl}-fea"
    @f.print <<EOF

// Click IP FEA output
mac_table[#{@fea_output}] -> Strip(14) -> CheckIPHeader -> ip_fea

_xorp_rt4[#{num += 1}] -> egress_ip
EOF

    node.iface.each { |i|
      if i.neighbor
        nbr = i.neighbor.node.label
        @f.print <<EOF
_xorp_rt4[#{num += 1}] -> EtherEncap(0x0800, #{fea}, #{nbr}-fea) -> tunnel#{num}
EOF
      end
    }

    @f.print <<EOF
_xorp_rt4[#{num += 1}] -> EtherEncap(0x0800, #{fea}, #{lbl}-eth0) -> uml
_xorp_rt4[#{num += 1}] -> ping :: IPClassifier(icmp type echo, -);
  ping[0] -> ICMPPingResponder -> ip_fea
  ping[1] -> EtherEncap(0x0800, #{fea}, #{lbl}-tap0) -> tap0
_xorp_rt4[#{num += 1}] -> EtherEncap(0x0800, #{fea}, egress-local) -> uml
EOF

    node.iface.each { |i|
      if i.neighbor
        nbr = i.neighbor.node.label
        @f.print <<EOF
_xorp_rt4[#{num += 1}] -> EtherEncap(0x0800, #{fea}, #{lbl}-#{nbr}) -> uml
EOF
      end
    }
  end

  def printOpenVPNOutput(node)
    if (! node.run_openvpn)
      @f.print <<EOF

// OpenVPN client output disabled (OpenVPN not running on this node)
mac_table[#{@client_output}] -> Discard
EOF
      if @click_forwarding
        @f.print "_xorp_rt4[0] -> Discard\n"
      end

      return
    end

    @f.print <<EOF

// OpenVPN server uses this UNIX socket as its tun/tap transport
openvpn :: Socket(UNIX_DGRAM, "/tmp/click.sock")
EOF
    if (@click_forwarding)
      @f.print <<EOF
-> CheckIPHeader -> ip_fea
_xorp_rt4[0] -> openvpn
EOF
    else
      @f.print "-> EtherEncap(0x0800, clientnet, #{node.label}-eth0) -> mac_table\n"
    end

    @f.print "mac_table[#{@client_output}] -> Strip(14) -> openvpn\n"
  end

  def printNAPTElements
    @f.print <<EOF

// Elements for doing NAT translation.
// This is ugly but hopefully you won't need to change it.
// It expects IP packets on its single input, and it outputs IP packets
// as well.
elementclass NaptEgress {
  $iface |

  // Source rewriter using port range 50000-65535
  IPRewriterPatterns(to_world_pat $iface 50000-65535 - -)

  rw :: IPRewriter(
    pattern to_world_pat 0 1,
    drop
  )

  input -> CheckIPHeader
  -> outc :: IPClassifier (tcp or udp, icmp type echo);

  // Handle TCP and UDP
  outc[0] -> af :: AggregateIPFlows(ICMP true) -> cp :: CheckPaint(0)
  
  // Outgoing packets sent here, incoming packets originate here
  tcpudpsock :: IPFlowRawSockets(NOTIFIER af, PCAP false)
  -> CheckIPHeader(INTERFACES $iface) 
  -> [1]rw

  // Outgoing packets
  cp[0]
  -> [0]rw
  -> GetIPAddress(16)
  -> CheckIPHeader
  -> tcpudpsock

  // Incoming packets
  rw[1]
  -> af

  cp[1]
   -> check :: IPClassifier(tcp, udp)

  // Work around an apparent bug in checksum calculation in IPRewriter
  check[0]
  -> SetTCPChecksum
  -> CheckIPHeader
  -> output

  check[1]
  -> SetUDPChecksum
  -> CheckIPHeader
  -> output

  // Handle ICMP 
  outc[1] -> pingrw :: ICMPPingRewriter($iface, -)

  icmpsock :: Queue 
  -> RawSocket("ICMP") 
  -> icmpcl :: IPClassifier(icmp type echo-reply, 
       icmp type timeexceeded or icmp type unreachable)
  -> CheckIPHeader(INTERFACES $iface) 
  -> [1]pingrw

  // Outgoing echo request packets
  pingrw[0]
  -> GetIPAddress(16)
  -> CheckIPHeader
  -> icmpsock

  // Incoming echo reply packets
  pingrw[1]
  -> CheckIPHeader
  -> output

  // Incoming ICMP error packets
  icmpcl[1]
  -> CheckIPHeader(INTERFACES $iface) 
  -> ICMPRewriter(rw)
  -> CheckIPHeader
  -> output
} // elementclass NaptEgress

EOF
  end

end

