| 43 | | |
|---|
| 44 | | |
|---|
| 45 | | |
|---|
| 46 | | ############################################################################# |
|---|
| 47 | | ############################################################################# |
|---|
| 48 | | ### Routing/Interfaces stuff ### |
|---|
| 49 | | ############################################################################# |
|---|
| 50 | | ############################################################################# |
|---|
| 51 | | |
|---|
| 52 | | def construct_source_candidate_set(addr, plen, laddr): |
|---|
| 53 | | """ |
|---|
| 54 | | Given all addresses assigned to a specific interface ('laddr' parameter), |
|---|
| 55 | | this function returns the "candidate set" associated with 'addr/plen'. |
|---|
| 56 | | |
|---|
| 57 | | Basically, the function filters all interface addresses to keep only those |
|---|
| 58 | | that have the same scope as provided prefix. |
|---|
| 59 | | |
|---|
| 60 | | This is on this list of addresses that the source selection mechanism |
|---|
| 61 | | will then be performed to select the best source address associated |
|---|
| 62 | | with some specific destination that uses this prefix. |
|---|
| 63 | | """ |
|---|
| 64 | | |
|---|
| 65 | | cset = [] |
|---|
| 66 | | if in6_isgladdr(addr): |
|---|
| 67 | | cset = filter(lambda x: x[1] == IPV6_ADDR_GLOBAL, laddr) |
|---|
| 68 | | elif in6_islladdr(addr): |
|---|
| 69 | | cset = filter(lambda x: x[1] == IPV6_ADDR_LINKLOCAL, laddr) |
|---|
| 70 | | elif in6_issladdr(addr): |
|---|
| 71 | | cset = filter(lambda x: x[1] == IPV6_ADDR_SITELOCAL, laddr) |
|---|
| 72 | | elif in6_ismaddr(addr): |
|---|
| 73 | | if in6_ismnladdr(addr): |
|---|
| 74 | | cset = [('::1', 16, LOOPBACK_NAME)] |
|---|
| 75 | | elif in6_ismgladdr(addr): |
|---|
| 76 | | cset = filter(lambda x: x[1] == IPV6_ADDR_GLOBAL, laddr) |
|---|
| 77 | | elif in6_ismlladdr(addr): |
|---|
| 78 | | cset = filter(lambda x: x[1] == IPV6_ADDR_LINKLOCAL, laddr) |
|---|
| 79 | | elif in6_ismsladdr(addr): |
|---|
| 80 | | cset = filter(lambda x: x[1] == IPV6_ADDR_SITELOCAL, laddr) |
|---|
| 81 | | elif addr == '::' and plen == 0: |
|---|
| 82 | | cset = filter(lambda x: x[1] == IPV6_ADDR_GLOBAL, laddr) |
|---|
| 83 | | cset = map(lambda x: x[0], cset) |
|---|
| 84 | | return cset |
|---|
| 85 | | |
|---|
| 86 | | def get_source_addr_from_candidate_set(dst, candidate_set): |
|---|
| 87 | | """ |
|---|
| 88 | | This function implement a limited version of source address selection |
|---|
| 89 | | algorithm defined in section 5 of RFC 3484. The format is very different |
|---|
| 90 | | from that described in the document because it operates on a set |
|---|
| 91 | | of candidate source address for some specific route. |
|---|
| 92 | | |
|---|
| 93 | | Rationale behind the implementation is to be able to make the right |
|---|
| 94 | | choice for a 6to4 destination when both a 6to4 address and a IPv6 native |
|---|
| 95 | | address are available for that interface. |
|---|
| 96 | | """ |
|---|
| 97 | | |
|---|
| 98 | | if len(candidate_set) == 0: |
|---|
| 99 | | # Should not happen |
|---|
| 100 | | return None |
|---|
| 101 | | |
|---|
| 102 | | if in6_isaddr6to4(dst): |
|---|
| 103 | | tmp = filter(lambda x: in6_isaddr6to4(x), candidate_set) |
|---|
| 104 | | if len(tmp) != 0: |
|---|
| 105 | | return tmp[0] |
|---|
| 106 | | |
|---|
| 107 | | return candidate_set[0] |
|---|
| 108 | | |
|---|
| 109 | | class Route6: |
|---|
| 110 | | |
|---|
| 111 | | def __init__(self): |
|---|
| 112 | | self.invalidate_cache() |
|---|
| 113 | | self.resync() |
|---|
| 114 | | |
|---|
| 115 | | def invalidate_cache(self): |
|---|
| 116 | | self.cache = {} |
|---|
| 117 | | |
|---|
| 118 | | def flush(self): |
|---|
| 119 | | self.invalidate_cache() |
|---|
| 120 | | self.routes = [] |
|---|
| 121 | | |
|---|
| 122 | | def resync(self): |
|---|
| 123 | | # TODO : At the moment, resync will drop existing Teredo routes |
|---|
| 124 | | # if any. Change that ... |
|---|
| 125 | | self.invalidate_cache() |
|---|
| 126 | | self.routes = read_routes6() |
|---|
| 127 | | if self.routes == []: |
|---|
| 128 | | log_loading.info("No IPv6 support in kernel") |
|---|
| 129 | | |
|---|
| 130 | | def __repr__(self): |
|---|
| 131 | | rtlst = [('Destination', 'Next Hop', "iface", "src candidates")] |
|---|
| 132 | | |
|---|
| 133 | | for net,msk,gw,iface,cset in self.routes: |
|---|
| 134 | | rtlst.append(('%s/%i'% (net,msk), gw, iface, ", ".join(cset))) |
|---|
| 135 | | |
|---|
| 136 | | colwidth = map(lambda x: max(map(lambda y: len(y), x)), apply(zip, rtlst)) |
|---|
| 137 | | fmt = " ".join(map(lambda x: "%%-%ds"%x, colwidth)) |
|---|
| 138 | | rt = "\n".join(map(lambda x: fmt % x, rtlst)) |
|---|
| 139 | | |
|---|
| 140 | | return rt |
|---|
| 141 | | |
|---|
| 142 | | |
|---|
| 143 | | # Unlike Scapy's Route.make_route() function, we do not have 'host' and 'net' |
|---|
| 144 | | # parameters. We only have a 'dst' parameter that accepts 'prefix' and |
|---|
| 145 | | # 'prefix/prefixlen' values. |
|---|
| 146 | | # WARNING: Providing a specific device will at the moment not work correctly. |
|---|
| 147 | | def make_route(self, dst, gw=None, dev=None): |
|---|
| 148 | | """Internal function : create a route for 'dst' via 'gw'. |
|---|
| 149 | | """ |
|---|
| 150 | | prefix, plen = (dst.split("/")+["128"])[:2] |
|---|
| 151 | | plen = int(plen) |
|---|
| 152 | | |
|---|
| 153 | | if gw is None: |
|---|
| 154 | | gw = "::" |
|---|
| 155 | | if dev is None: |
|---|
| 156 | | dev, ifaddr, x = self.route(gw) |
|---|
| 157 | | else: |
|---|
| 158 | | # TODO: do better than that |
|---|
| 159 | | # replace that unique address by the list of all addresses |
|---|
| 160 | | lifaddr = in6_getifaddr() |
|---|
| 161 | | devaddrs = filter(lambda x: x[2] == dev, lifaddr) |
|---|
| 162 | | ifaddr = construct_source_candidate_set(prefix, plen, devaddrs) |
|---|
| 163 | | |
|---|
| 164 | | return (prefix, plen, gw, dev, ifaddr) |
|---|
| 165 | | |
|---|
| 166 | | |
|---|
| 167 | | def add(self, *args, **kargs): |
|---|
| 168 | | """Ex: |
|---|
| 169 | | add(dst="2001:db8:cafe:f000::/56") |
|---|
| 170 | | add(dst="2001:db8:cafe:f000::/56", gw="2001:db8:cafe::1") |
|---|
| 171 | | add(dst="2001:db8:cafe:f000::/64", gw="2001:db8:cafe::1", dev="eth0") |
|---|
| 172 | | """ |
|---|
| 173 | | self.invalidate_cache() |
|---|
| 174 | | self.routes.append(self.make_route(*args, **kargs)) |
|---|
| 175 | | |
|---|
| 176 | | |
|---|
| 177 | | def delt(self, dst, gw=None): |
|---|
| 178 | | """ Ex: |
|---|
| 179 | | delt(dst="::/0") |
|---|
| 180 | | delt(dst="2001:db8:cafe:f000::/56") |
|---|
| 181 | | delt(dst="2001:db8:cafe:f000::/56", gw="2001:db8:deca::1") |
|---|
| 182 | | """ |
|---|
| 183 | | tmp = dst+"/128" |
|---|
| 184 | | dst, plen = tmp.split('/')[:2] |
|---|
| 185 | | dst = in6_ptop(dst) |
|---|
| 186 | | plen = int(plen) |
|---|
| 187 | | l = filter(lambda x: in6_ptop(x[0]) == dst and x[1] == plen, self.routes) |
|---|
| 188 | | if gw: |
|---|
| 189 | | gw = in6_ptop(gw) |
|---|
| 190 | | l = filter(lambda x: in6_ptop(x[0]) == gw, self.routes) |
|---|
| 191 | | if len(l) == 0: |
|---|
| 192 | | warning("No matching route found") |
|---|
| 193 | | elif len(l) > 1: |
|---|
| 194 | | warning("Found more than one match. Aborting.") |
|---|
| 195 | | else: |
|---|
| 196 | | i=self.routes.index(l[0]) |
|---|
| 197 | | self.invalidate_cache() |
|---|
| 198 | | del(self.routes[i]) |
|---|
| 199 | | |
|---|
| 200 | | def ifchange(self, iff, addr): |
|---|
| 201 | | the_addr, the_plen = (addr.split("/")+["128"])[:2] |
|---|
| 202 | | the_plen = int(the_plen) |
|---|
| 203 | | |
|---|
| 204 | | naddr = inet_pton(socket.AF_INET6, the_addr) |
|---|
| 205 | | nmask = in6_cidr2mask(the_plen) |
|---|
| 206 | | the_net = inet_ntop(socket.AF_INET6, in6_and(nmask,naddr)) |
|---|
| 207 | | |
|---|
| 208 | | for i in range(len(self.routes)): |
|---|
| 209 | | net,plen,gw,iface,addr = self.routes[i] |
|---|
| 210 | | if iface != iff: |
|---|
| 211 | | continue |
|---|
| 212 | | if gw == '::': |
|---|
| 213 | | self.routes[i] = (the_net,the_plen,gw,iface,the_addr) |
|---|
| 214 | | else: |
|---|
| 215 | | self.routes[i] = (net,the_plen,gw,iface,the_addr) |
|---|
| 216 | | self.invalidate_cache() |
|---|
| 217 | | ip6_neigh_cache.flush() |
|---|
| 218 | | |
|---|
| 219 | | def ifdel(self, iff): |
|---|
| 220 | | """ removes all route entries that uses 'iff' interface. """ |
|---|
| 221 | | new_routes=[] |
|---|
| 222 | | for rt in self.routes: |
|---|
| 223 | | if rt[3] != iff: |
|---|
| 224 | | new_routes.append(rt) |
|---|
| 225 | | self.invalidate_cache() |
|---|
| 226 | | self.routes = new_routes |
|---|
| 227 | | |
|---|
| 228 | | |
|---|
| 229 | | def ifadd(self, iff, addr): |
|---|
| 230 | | """ |
|---|
| 231 | | Add an interface 'iff' with provided address into routing table. |
|---|
| 232 | | |
|---|
| 233 | | Ex: ifadd('eth0', '2001:bd8:cafe:1::1/64') will add following entry into |
|---|
| 234 | | Scapy6 internal routing table: |
|---|
| 235 | | |
|---|
| 236 | | Destination Next Hop iface Def src @ |
|---|
| 237 | | 2001:bd8:cafe:1::/64 :: eth0 2001:bd8:cafe:1::1 |
|---|
| 238 | | |
|---|
| 239 | | prefix length value can be omitted. In that case, a value of 128 |
|---|
| 240 | | will be used. |
|---|
| 241 | | """ |
|---|
| 242 | | addr, plen = (addr.split("/")+["128"])[:2] |
|---|
| 243 | | addr = in6_ptop(addr) |
|---|
| 244 | | plen = int(plen) |
|---|
| 245 | | naddr = inet_pton(socket.AF_INET6, addr) |
|---|
| 246 | | nmask = in6_cidr2mask(plen) |
|---|
| 247 | | prefix = inet_ntop(socket.AF_INET6, in6_and(nmask,naddr)) |
|---|
| 248 | | self.invalidate_cache() |
|---|
| 249 | | self.routes.append((prefix,plen,'::',iff,[addr])) |
|---|
| 250 | | |
|---|
| 251 | | def route(self, dst, dev=None): |
|---|
| 252 | | """ |
|---|
| 253 | | Provide best route to IPv6 destination address, based on Scapy6 |
|---|
| 254 | | internal routing table content. |
|---|
| 255 | | |
|---|
| 256 | | When a set of address is passed (e.g. 2001:db8:cafe:*::1-5) an address |
|---|
| 257 | | of the set is used. Be aware of that behavior when using wildcards in |
|---|
| 258 | | upper parts of addresses ! |
|---|
| 259 | | |
|---|
| 260 | | If 'dst' parameter is a FQDN, name resolution is performed and result |
|---|
| 261 | | is used. |
|---|
| 262 | | |
|---|
| 263 | | if optional 'dev' parameter is provided a specific interface, filtering |
|---|
| 264 | | is performed to limit search to route associated to that interface. |
|---|
| 265 | | """ |
|---|
| 266 | | # Transform "2001:db8:cafe:*::1-5:0/120" to one IPv6 address of the set |
|---|
| 267 | | dst = dst.split("/")[0] |
|---|
| 268 | | savedst = dst # In case following inet_pton() fails |
|---|
| 269 | | dst = dst.replace("*","0") |
|---|
| 270 | | l = dst.find("-") |
|---|
| 271 | | while l >= 0: |
|---|
| 272 | | m = (dst[l:]+":").find(":") |
|---|
| 273 | | dst = dst[:l]+dst[l+m:] |
|---|
| 274 | | l = dst.find("-") |
|---|
| 275 | | |
|---|
| 276 | | try: |
|---|
| 277 | | inet_pton(socket.AF_INET6, dst) |
|---|
| 278 | | except socket.error: |
|---|
| 279 | | dst = socket.getaddrinfo(savedst, None, socket.AF_INET6)[0][-1][0] |
|---|
| 280 | | # TODO : Check if name resolution went well |
|---|
| 281 | | |
|---|
| 282 | | # Deal with dev-specific request for cache search |
|---|
| 283 | | k = dst |
|---|
| 284 | | if dev is not None: |
|---|
| 285 | | k = dst + "%%" + dev |
|---|
| 286 | | if k in self.cache: |
|---|
| 287 | | return self.cache[k] |
|---|
| 288 | | |
|---|
| 289 | | pathes = [] |
|---|
| 290 | | |
|---|
| 291 | | # TODO : review all kinds of addresses (scope and *cast) to see |
|---|
| 292 | | # if we are able to cope with everything possible. I'm convinced |
|---|
| 293 | | # it's not the case. |
|---|
| 294 | | # -- arnaud |
|---|
| 295 | | for p, plen, gw, iface, cset in self.routes: |
|---|
| 296 | | if dev is not None and iface != dev: |
|---|
| 297 | | continue |
|---|
| 298 | | if in6_isincluded(dst, p, plen): |
|---|
| 299 | | pathes.append((plen, (iface, cset, gw))) |
|---|
| 300 | | elif (in6_ismlladdr(dst) and in6_islladdr(p) and in6_islladdr(cset[0])): |
|---|
| 301 | | pathes.append((plen, (iface, cset, gw))) |
|---|
| 302 | | |
|---|
| 303 | | if not pathes: |
|---|
| 304 | | warning("No route found for IPv6 destination %s (no default route?)" % dst) |
|---|
| 305 | | return (LOOPBACK_NAME, "::", "::") # XXX Linux specific |
|---|
| 306 | | |
|---|
| 307 | | pathes.sort() |
|---|
| 308 | | pathes.reverse() |
|---|
| 309 | | |
|---|
| 310 | | best_plen = pathes[0][0] |
|---|
| 311 | | pathes = filter(lambda x: x[0] == best_plen, pathes) |
|---|
| 312 | | |
|---|
| 313 | | res = [] |
|---|
| 314 | | for p in pathes: # Here we select best source address for every route |
|---|
| 315 | | tmp = p[1] |
|---|
| 316 | | srcaddr = get_source_addr_from_candidate_set(dst, p[1][1]) |
|---|
| 317 | | if srcaddr is not None: |
|---|
| 318 | | res.append((p[0], (tmp[0], srcaddr, tmp[2]))) |
|---|
| 319 | | |
|---|
| 320 | | # Symptom : 2 routes with same weight (our weight is plen) |
|---|
| 321 | | # Solution : |
|---|
| 322 | | # - dst is unicast global. Check if it is 6to4 and we have a source |
|---|
| 323 | | # 6to4 address in those available |
|---|
| 324 | | # - dst is link local (unicast or multicast) and multiple output |
|---|
| 325 | | # interfaces are available. Take main one (conf.iface6) |
|---|
| 326 | | # - if none of the previous or ambiguity persists, be lazy and keep |
|---|
| 327 | | # first one |
|---|
| 328 | | # XXX TODO : in a _near_ future, include metric in the game |
|---|
| 329 | | |
|---|
| 330 | | if len(res) > 1: |
|---|
| 331 | | tmp = [] |
|---|
| 332 | | if in6_isgladdr(dst) and in6_isaddr6to4(dst): |
|---|
| 333 | | # TODO : see if taking the longest match between dst and |
|---|
| 334 | | # every source addresses would provide better results |
|---|
| 335 | | tmp = filter(lambda x: in6_isaddr6to4(x[1][1]), res) |
|---|
| 336 | | elif in6_ismaddr(dst) or in6_islladdr(dst): |
|---|
| 337 | | # TODO : I'm sure we are not covering all addresses. Check that |
|---|
| 338 | | tmp = filter(lambda x: x[1][0] == conf.iface6, res) |
|---|
| 339 | | |
|---|
| 340 | | if tmp: |
|---|
| 341 | | res = tmp |
|---|
| 342 | | |
|---|
| 343 | | # Fill the cache (including dev-specific request) |
|---|
| 344 | | k = dst |
|---|
| 345 | | if dev is not None: |
|---|
| 346 | | k = dst + "%%" + dev |
|---|
| 347 | | self.cache[k] = res[0][1] |
|---|
| 348 | | |
|---|
| 349 | | return res[0][1] |
|---|
| 350 | | |
|---|