Replace shelling out to ip with netlink code.

This reduces test setup time by about 50%.

Change-Id: I079bd29e5366eb7b23e63b2f4f3dbd9267376cca
diff --git a/net/test/iproute.py b/net/test/iproute.py
index 1b544d6..a899cb7 100644
--- a/net/test/iproute.py
+++ b/net/test/iproute.py
@@ -37,6 +37,12 @@
 
 ### rtnetlink constants. See include/uapi/linux/rtnetlink.h.
 # Message types.
+RTM_NEWADDR = 20
+RTM_DELADDR = 21
+RTM_NEWROUTE = 24
+RTM_DELROUTE = 25
+RTM_NEWNEIGH = 28
+RTM_DELNEIGH = 29
 RTM_NEWRULE = 32
 RTM_DELRULE = 33
 RTM_GETRULE = 34
@@ -55,12 +61,45 @@
 # Named routing tables.
 RT_TABLE_UNSPEC = 0
 
+# Routing attributes.
+RTA_DST = 1
+RTA_OIF = 4
+RTA_GATEWAY = 5
+
 # Data structure formats.
 RTMsg = cstruct.Struct(
     "RTMsg", "=BBBBBBBBI",
     "family dst_len src_len tos table protocol scope type flags")
 
 
+### Interface address constants. See include/uapi/linux/if_addr.h.
+# Interface address attributes.
+IFA_ADDRESS = 1
+IFA_LOCAL = 2
+
+# Address flags.
+IFA_F_PERMANENT = 0x80
+
+# Data structure formats.
+IfAddrMsg = cstruct.Struct(
+    "IfAddrMsg", "=BBBBI",
+    "family prefixlen flags scope index")
+
+
+### Neighbour table entry constants. See include/uapi/linux/neighbour.h.
+# Neighbour cache entry attributes.
+NDA_DST = 1
+NDA_LLADDR = 2
+
+# Neighbour cache entry states.
+NUD_PERMANENT = 0x80
+
+# Data structure formats.
+NdMsg = cstruct.Struct(
+    "NdMsg", "=BxxxiHBB",
+    "family ifindex state flags type")
+
+
 ### FIB rule constants. See include/uapi/linux/fib_rules.h.
 FRA_PRIORITY = 6
 FRA_FWMARK = 10
@@ -96,6 +135,9 @@
   def _NlAttrU32(self, nla_type, value):
     return self._NlAttr(nla_type, struct.pack("=I", value))
 
+  def _NlAttrIPAddress(self, nla_type, family, address):
+    return self._NlAttr(nla_type, socket.inet_pton(family, address))
+
   def __init__(self):
     # Global sequence number.
     self.seq = 0
@@ -105,6 +147,7 @@
     self.pid = self.sock.getsockname()[1]
 
   def _Send(self, msg):
+    self._Debug(msg.encode("hex"))
     self.seq += 1
     self.sock.send(msg)
 
@@ -129,6 +172,24 @@
     else:
       raise ValueError("Unexpected netlink ACK type %d" % hdr.type)
 
+  def _AddressFamily(self, version):
+    return {4: socket.AF_INET, 6: socket.AF_INET6}[version]
+
+  def _SendNlRequest(self, command, is_add, data):
+    """Sends a netlink request and expects an ack."""
+    flags = NLM_F_REQUEST | NLM_F_ACK
+    if is_add:
+      flags |= (NLM_F_EXCL | NLM_F_CREATE)
+
+    length = len(NLMsgHdr) + len(data)
+    nlmsg = NLMsgHdr((length, command, flags, self.seq, self.pid)).Pack()
+
+    # Send the message and block forever until we receive a response.
+    self._Send(nlmsg + data)
+
+    # Expect a successful ACK.
+    self._ExpectAck()
+
   def _Rule(self, version, is_add, table, match_nlattr, priority):
     """Python equivalent of "ip rule <add|del> <match_cond> lookup <table>".
 
@@ -143,10 +204,8 @@
       IOError: If the netlink request returns an error.
       ValueError: If the kernel's response could not be parsed.
     """
-    self.seq += 1
-
     # Create a struct rtmsg specifying the table and the given match attributes.
-    family = {4: socket.AF_INET, 6: socket.AF_INET6}[version]
+    family = self._AddressFamily(version)
     rtmsg = RTMsg((family, 0, 0, 0, RT_TABLE_UNSPEC,
                    RTPROT_STATIC, RT_SCOPE_UNIVERSE, RTN_UNICAST, 0)).Pack()
     rtmsg += self._NlAttrU32(FRA_PRIORITY, priority)
@@ -155,18 +214,7 @@
 
     # Create a netlink request containing the rtmsg.
     command = RTM_NEWRULE if is_add else RTM_DELRULE
-    flags = NLM_F_REQUEST | NLM_F_ACK
-    if is_add:
-      flags |= (NLM_F_EXCL | NLM_F_CREATE)
-
-    length = len(NLMsgHdr) + len(rtmsg)
-    nlmsg = NLMsgHdr((length, command, flags, self.seq, self.pid)).Pack()
-
-    # Send the message and block forever until we receive a response.
-    self._Send(nlmsg + rtmsg)
-
-    # Expect a successful ACK.
-    self._ExpectAck()
+    self._SendNlRequest(command, is_add, rtmsg)
 
   def FwmarkRule(self, version, is_add, fwmark, table, priority=16383):
     nlattr = self._NlAttrU32(FRA_FWMARK, fwmark)
@@ -184,7 +232,7 @@
   def DumpRules(self, version):
     """Returns the IP rules for the specified IP version."""
     # Create a struct rtmsg specifying the table and the given match attributes.
-    family = {4: socket.AF_INET, 6: socket.AF_INET6}[version]
+    family = self._AddressFamily(version)
     rtmsg = RTMsg((family, 0, 0, 0, 0, 0, 0, 0, 0))
 
     # Create a netlink dump request containing the rtmsg.
@@ -226,6 +274,67 @@
     self._ExpectDone()
     return rules
 
+  def _Address(self, version, is_add, addr, prefixlen, flags, scope, ifindex):
+    """Adds or deletes an IP address."""
+    family = self._AddressFamily(version)
+    ifaddrmsg = IfAddrMsg((family, prefixlen, flags, scope, ifindex)).Pack()
+    ifaddrmsg += self._NlAttrIPAddress(IFA_ADDRESS, family, addr)
+    if version == 4:
+      ifaddrmsg += self._NlAttrIPAddress(IFA_LOCAL, family, addr)
+    command = RTM_NEWADDR if is_add else RTM_DELADDR
+    self._SendNlRequest(command, is_add, ifaddrmsg)
+
+  def AddAddress(self, address, prefixlen, ifindex):
+    version = 6 if ":" in address else 4
+    return self._Address(version, True, address, prefixlen, IFA_F_PERMANENT,
+                         RT_SCOPE_UNIVERSE, ifindex)
+
+  def DelAddress(self, address, prefixlen, ifindex):
+    version = 6 if ":" in address else 4
+    return self._Address(version, False, address, prefixlen, 0, 0, ifindex)
+
+  def _Route(self, version, is_add, table, dest, prefixlen, nexthop, dev):
+    """Adds or deletes a route."""
+    family = self._AddressFamily(version)
+    rtmsg = RTMsg((family, prefixlen, 0, 0, RT_TABLE_UNSPEC,
+                   RTPROT_STATIC, RT_SCOPE_UNIVERSE, RTN_UNICAST, 0)).Pack()
+    rtmsg += self._NlAttrU32(FRA_TABLE, table)
+    if dest != "default":  # The default is the default route.
+      rtmsg += self._NlAttrIPAddress(RTA_DST, family, dest)
+    rtmsg += self._NlAttrIPAddress(RTA_GATEWAY, family, nexthop)
+    rtmsg += self._NlAttrU32(RTA_OIF, dev)
+    command = RTM_NEWROUTE if is_add else RTM_DELROUTE
+    self._SendNlRequest(command, is_add, rtmsg)
+
+  def AddRoute(self, version, table, dest, prefixlen, nexthop, dev):
+    self._Route(version, True, table, dest, prefixlen, nexthop, dev)
+
+  def DelRoute(self, version, table, dest, prefixlen, nexthop, dev):
+    self._Route(version, False, table, dest, prefixlen, nexthop, dev)
+
+  def _Neighbour(self, version, is_add, addr, lladdr, dev, state):
+    """Adds or deletes a neighbour cache entry."""
+    family = self._AddressFamily(version)
+
+    # Convert the link-layer address to a raw byte string.
+    if is_add:
+      lladdr = lladdr.split(":")
+      if len(lladdr) != 6:
+        raise ValueError("Invalid lladdr %s" % ":".join(lladdr))
+      lladdr = "".join(chr(int(hexbyte, 16)) for hexbyte in lladdr)
+
+    ndmsg = NdMsg((family, dev, state, 0, RTN_UNICAST)).Pack()
+    ndmsg += self._NlAttrIPAddress(NDA_DST, family, addr)
+    ndmsg += self._NlAttr(NDA_LLADDR, lladdr)
+    command = RTM_NEWNEIGH if is_add else RTM_DELNEIGH
+    self._SendNlRequest(command, is_add, ndmsg)
+
+  def AddNeighbour(self, version, addr, lladdr, dev):
+    self._Neighbour(version, True, addr, lladdr, dev, NUD_PERMANENT)
+
+  def DelNeighbour(self, version, addr, lladdr, dev):
+    self._Neighbour(version, False, addr, lladdr, dev, 0)
+
 
 if __name__ == "__main__":
   iproute = IPRoute()
diff --git a/net/test/mark_test.py b/net/test/mark_test.py
index 432f4d1..3411dd6 100755
--- a/net/test/mark_test.py
+++ b/net/test/mark_test.py
@@ -363,23 +363,25 @@
 
   @classmethod
   def _RunSetupCommands(cls, netid, is_add):
-    iptables_commands = [
-        "/sbin/%(iptables)s %(append_delete)s INPUT -t mangle -i %(iface)s"
-        " -j MARK --set-mark %(mark)d",
-    ]
-    route_commands = [
-        "ip -%(version)d route %(add_del)s table %(table)s"
-        " default dev %(iface)s via %(router)s",
-    ]
-    ipv4_commands = [
-        "ip -4 nei %(add_del)s %(router)s dev %(iface)s"
-        " lladdr %(macaddr)s nud permanent",
-        "ip -4 addr %(add_del)s %(ipv4addr)s/24 dev %(iface)s",
-    ]
-
-    for version, iptables in zip([4, 6], ["iptables", "ip6tables"]):
-      table = cls._TableForNetid(netid)
+    for version in [4, 6]:
+      # Find out how to configure things.
       iface = cls.GetInterfaceName(netid)
+      ifindex = cls.ifindices[netid]
+      macaddr = cls.RouterMacAddress(netid)
+      router = cls._RouterAddress(netid, version)
+      table = cls._TableForNetid(netid)
+
+      # Run iptables to set up incoming packet marking.
+      add_del = "-A" if is_add else "-D"
+      iptables = {4: "iptables", 6: "ip6tables"}[version]
+      args = "%s %s INPUT -t mangle -i %s -j MARK --set-mark %d" % (
+          iptables, add_del, iface, netid)
+      iptables = "/sbin/" + iptables
+      ret = os.spawnvp(os.P_WAIT, iptables, args.split(" "))
+      if ret:
+        raise ConfigurationError("Setup command failed: %s" % args)
+
+      # Set up routing rules.
       if HAVE_EXPERIMENTAL_UID_ROUTING:
         start, end = cls.UidRangeForNetid(netid)
         cls.iproute.UidRangeRule(version, is_add, start, end, table,
@@ -387,42 +389,28 @@
       cls.iproute.OifRule(version, is_add, iface, table, priority=200)
       cls.iproute.FwmarkRule(version, is_add, netid, table, priority=300)
 
-      if cls.DEBUG:
-        os.spawnvp(os.P_WAIT, "/sbin/ip", ["ip", "-6", "rule", "list"])
-
-      if version == 6:
-        if cls.AUTOCONF_TABLE_OFFSET is None:
-          # Set up routing manually.
-          cmds = iptables_commands + route_commands
-        else:
-          cmds = iptables_commands
-
-      if version == 4:
-        # Deleting addresses also causes routes to be deleted, so watch the
-        # order or the test will output lots of ENOENT errors.
-        if is_add:
-          cmds = iptables_commands + ipv4_commands + route_commands
-        else:
-          cmds = iptables_commands + route_commands + ipv4_commands
-
-      cmds = str("\n".join(cmds) % {
-          "add_del": "add" if is_add else "del",
-          "append_delete": "-A" if is_add else "-D",
-          "iface": iface,
-          "iptables": iptables,
-          "ipv4addr": cls._MyIPv4Address(netid),
-          "macaddr": cls.RouterMacAddress(netid),
-          "mark": netid,
-          "router": cls._RouterAddress(netid, version),
-          "table": table,
-          "version": version,
-      }).split("\n")
-      for cmd in cmds:
-        cmd = cmd.split(" ")
-        if cls.DEBUG: print " ".join(cmd)
-        ret = os.spawnvp(os.P_WAIT, cmd[0], cmd)
-        if ret:
-          raise ConfigurationError("Setup command failed: %s" % " ".join(cmd))
+      # Configure routing and addressing.
+      #
+      # IPv6 uses autoconf for everything, except if per-device autoconf routing
+      # tables are not supported, in which case the default route (only) is
+      # configured manually. For IPv4 we have to manualy configure addresses,
+      # routes, and neighbour cache entries (since we don't reply to ARP or ND).
+      #
+      # Since deleting addresses also causes routes to be deleted, we need to
+      # be careful with ordering or the delete commands will fail with ENOENT.
+      do_routing = (version == 4 or cls.AUTOCONF_TABLE_OFFSET is None)
+      if is_add:
+        if version == 4:
+          cls.iproute.AddAddress(cls._MyIPv4Address(netid), 24, ifindex)
+          cls.iproute.AddNeighbour(version, router, macaddr, ifindex)
+        if do_routing:
+          cls.iproute.AddRoute(version, table, "default", 0, router, ifindex)
+      else:
+        if do_routing:
+          cls.iproute.DelRoute(version, table, "default", 0, router, ifindex)
+        if version == 4:
+          cls.iproute.DelNeighbour(version, router, macaddr, ifindex)
+          cls.iproute.DelAddress(cls._MyIPv4Address(netid), 24, ifindex)
 
   @classmethod
   def GetSysctl(cls, sysctl):
@@ -1030,6 +1018,7 @@
         self.SendRA(netid)
       CheckIPv6Connectivity(True)
 
+  @unittest.skipUnless(HAVE_AUTOCONF_TABLE, "our manual routing doesn't do PIO")
   def testOnlinkCommunication(self):
     """Checks that on-link communication goes direct and not through routers."""
     for netid in self.tuns: