#include <RoutingTable6.h>
See the NED documentation for general overview.
This is a simple module without gates, it requires function calls to it (message handling does nothing). Methods are provided for reading and updating the interface table and the route table, as well as for unicast and multicast routing.
The route table is read from a file. The route table can also be read and modified during simulation, typically by routing protocol implementations.
Public Member Functions | |
RoutingTable6 () | |
virtual | ~RoutingTable6 () |
bool | isRouter () const |
Interfaces | |
InterfaceEntry * | interfaceByAddress (const IPv6Address &address) |
Routing functions | |
bool | localDeliver (const IPv6Address &dest) |
const IPv6Address & | lookupDestCache (const IPv6Address &dest, int &outInterfaceId) |
const IPv6Route * | doLongestPrefixMatch (const IPv6Address &dest) |
bool | isPrefixPresent (const IPv6Address &prefix) |
Managing the destination cache | |
void | updateDestCache (const IPv6Address &dest, const IPv6Address &nextHopAddr, int interfaceId) |
void | purgeDestCache () |
void | purgeDestCacheEntriesToNeighbour (const IPv6Address &nextHopAddr, int interfaceId) |
Managing prefixes and the route table | |
void | addOrUpdateOnLinkPrefix (const IPv6Address &destPrefix, int prefixLength, int interfaceId, simtime_t expiryTime) |
void | removeOnLinkPrefix (const IPv6Address &destPrefix, int prefixLength) |
void | addOrUpdateOwnAdvPrefix (const IPv6Address &destPrefix, int prefixLength, int interfaceId, simtime_t expiryTime) |
void | addStaticRoute (const IPv6Address &destPrefix, int prefixLength, unsigned int interfaceId, const IPv6Address &nextHop, int metric=0) |
void | addDefaultRoute (const IPv6Address &raSrcAddr, unsigned int ifID, simtime_t routerLifetime) |
void | addRoutingProtocolRoute (IPv6Route *route) |
void | removeRoute (IPv6Route *route) |
int | numRoutes () const |
IPv6Route * | route (int i) |
Protected Member Functions | |
void | updateDisplayString () |
int | numInitStages () const |
void | initialize (int stage) |
void | parseXMLConfigFile () |
void | handleMessage (cMessage *) |
Private Types | |
typedef std::map< IPv6Address, DestCacheEntry > | DestCache |
typedef std::vector< IPv6Route * > | RouteList |
Private Member Functions | |
void | addRoute (IPv6Route *route) |
void | configureInterfaceForIPv6 (InterfaceEntry *ie) |
void | assignRequiredNodeAddresses (InterfaceEntry *ie) |
void | configureInterfaceFromXML (InterfaceEntry *ie, cXMLElement *cfg) |
Static Private Member Functions | |
static bool | routeLessThan (const IPv6Route *a, const IPv6Route *b) |
Private Attributes | |
InterfaceTable * | ift |
bool | isrouter |
DestCache | destCache |
RouteList | routeList |
Friends | |
std::ostream & | operator<< (std::ostream &os, const DestCacheEntry &e) |
Classes | |
struct | DestCacheEntry |
|
|
|
|
|
00075 { 00076 }
|
|
|
|
Adds a default route for a host. This method requires the RA's source address and the router expiry time plus the simTime(). 00552 { 00553 // create route object 00554 IPv6Route *route = new IPv6Route(IPv6Address(), 0, IPv6Route::FROM_RA); 00555 route->setInterfaceID(ifID); 00556 route->setNextHop(nextHop); 00557 route->setMetric(10);//FIXME:should be filled from interface metric 00558 00559 // then add it 00560 addRoute(route); 00561 }
|
|
Add on-link prefix (route of type FROM_RA), or update existing one. To be called from code processing on-link prefixes in Router Advertisements. Expiry time can be derived from the Valid Lifetime field in the Router Advertisements. NOTE: This method does NOT update the lifetime of matching addresses in the InterfaceTable (see IPv6InterfaceData); that has to be done separately. 00447 { 00448 // see if prefix exists in table 00449 IPv6Route *route = NULL; 00450 for (RouteList::iterator it=routeList.begin(); it!=routeList.end(); it++) 00451 { 00452 if ((*it)->src()==IPv6Route::FROM_RA && (*it)->destPrefix()==destPrefix && (*it)->prefixLength()==prefixLength) 00453 { 00454 route = *it; 00455 break; 00456 } 00457 } 00458 00459 if (route==NULL) 00460 { 00461 // create new route object 00462 IPv6Route *route = new IPv6Route(destPrefix, prefixLength, IPv6Route::FROM_RA); 00463 route->setInterfaceID(interfaceId); 00464 route->setExpiryTime(expiryTime); 00465 route->setMetric(0); 00466 00467 // then add it 00468 addRoute(route); 00469 } 00470 else 00471 { 00472 // update existing one 00473 route->setInterfaceID(interfaceId); 00474 route->setExpiryTime(expiryTime); 00475 } 00476 00477 updateDisplayString(); 00478 }
|
|
Add route of type OWN_ADV_PREFIX. This is a prefix that *this* router advertises on this interface. 00482 { 00483 // FIXME this is very similar to the one above -- refactor!! 00484 00485 // see if prefix exists in table 00486 IPv6Route *route = NULL; 00487 for (RouteList::iterator it=routeList.begin(); it!=routeList.end(); it++) 00488 { 00489 if ((*it)->src()==IPv6Route::OWN_ADV_PREFIX && (*it)->destPrefix()==destPrefix && (*it)->prefixLength()==prefixLength) 00490 { 00491 route = *it; 00492 break; 00493 } 00494 } 00495 00496 if (route==NULL) 00497 { 00498 // create new route object 00499 IPv6Route *route = new IPv6Route(destPrefix, prefixLength, IPv6Route::OWN_ADV_PREFIX); 00500 route->setInterfaceID(interfaceId); 00501 route->setExpiryTime(expiryTime); 00502 route->setMetric(0); 00503 00504 // then add it 00505 addRoute(route); 00506 } 00507 else 00508 { 00509 // update existing one 00510 route->setInterfaceID(interfaceId); 00511 route->setExpiryTime(expiryTime); 00512 } 00513 00514 updateDisplayString(); 00515 }
|
|
00580 { 00581 routeList.push_back(route); 00582 00583 // we keep entries sorted by prefix length in routeList, so that we can 00584 // stop at the first match when doing the longest prefix matching 00585 std::sort(routeList.begin(), routeList.end(), routeLessThan); 00586 00587 updateDisplayString(); 00588 }
|
|
Adds the given route (which can be OSPF, BGP, RIP or any other route) with src==ROUTING_PROT. To store additional information with the route, one can subclass from IPv6Route and add more fields. 00564 { 00565 ASSERT(route->src()==IPv6Route::ROUTING_PROT); 00566 addRoute(route); 00567 }
|
|
Creates a static route. If metric is omitted, it gets initialized to the interface's metric value. 00535 { 00536 // create route object 00537 IPv6Route *route = new IPv6Route(destPrefix, prefixLength, IPv6Route::STATIC); 00538 route->setInterfaceID(interfaceId); 00539 route->setNextHop(nextHop); 00540 if (metric==0) 00541 { 00542 metric = 10; // TBD should be filled from interface metric 00543 } 00544 route->setMetric(metric); 00545 00546 // then add it 00547 addRoute(route); 00548 }
|
|
RFC 3513: Section 2.8 A Node's Required Address Assign the various addresses to the node's respective interface. This should be done when the IPv6 Protocol stack is created. 00197 { 00198 //RFC 3513 Section 2.8:A Node's Required Addresses 00199 /*A host is required to recognize the following addresses as 00200 identifying itself:*/ 00201 00202 //o The loopback address. 00203 if (ie->isLoopback()) 00204 { 00205 ie->ipv6()->assignAddress(IPv6Address("::1"), false, 0, 0); 00206 return; 00207 } 00208 //o Its required Link-Local Address for each interface. 00209 //IPv6Address linkLocalAddr = IPv6Address().formLinkLocalAddress(ie->interfaceToken()); 00210 //ie->ipv6()->assignAddress(linkLocalAddr, true, 0, 0); 00211 00212 /*o Any additional Unicast and Anycast Addresses that have been configured 00213 for the node's interfaces (manually or automatically).*/ 00214 00215 // FIXME FIXME Andras: commented out the following lines, because these addresses 00216 // are implicitly checked for in localDeliver() (we don't want redundancy, 00217 // and manually adding solicited-node mcast address for each and every address 00218 // is very error-prone!) 00219 // 00220 //o The All-Nodes Multicast Addresses defined in section 2.7.1. 00221 00222 /*o The Solicited-Node Multicast Address for each of its unicast and anycast 00223 addresses.*/ 00224 00225 //o Multicast Addresses of all other groups to which the node belongs. 00226 00227 /*A router is required to recognize all addresses that a host is 00228 required to recognize, plus the following addresses as identifying 00229 itself:*/ 00230 /*o The Subnet-Router Anycast Addresses for all interfaces for 00231 which it is configured to act as a router.*/ 00232 00233 //o All other Anycast Addresses with which the router has been configured. 00234 //o The All-Routers Multicast Addresses defined in section 2.7.1. 00235 }
|
|
00179 { 00180 IPv6InterfaceData *ipv6IfData = new IPv6InterfaceData(); 00181 ie->setIPv6Data(ipv6IfData); 00182 00183 // for routers, turn on advertisements by default 00184 //FIXME: we will use this isRouter flag for now. what if future implementations 00185 //have 2 interfaces where one interface is configured as a router and the other 00186 //as a host? 00187 ipv6IfData->setAdvSendAdvertisements(isrouter);//Added by WEI 00188 00189 // metric: some hints: OSPF cost (2e9/bps value), MS KB article Q299540, ... 00190 //d->setMetric((int)ceil(2e9/ie->datarate())); // use OSPF cost as default 00191 //FIXME TBD fill in the rest 00192 00193 assignRequiredNodeAddresses(ie); 00194 }
|
|
00253 { 00254 /*XML parsing capabilities tweaked by WEI. For now, we can configure a specific 00255 node's interface. We can set advertising prefixes and other variables to be used 00256 in RAs. The IPv6 interface data gets overwritten if lines 249 to 262 is uncommented. 00257 The fix is to create an XML file with all the default values. Customised XML files 00258 can be used for future protocols that requires different values. (MIPv6)*/ 00259 IPv6InterfaceData *d = ie->ipv6(); 00260 00261 // parse basic config (attributes) 00262 d->setAdvSendAdvertisements(toBool(getRequiredAttr(cfg, "AdvSendAdvertisements"))); 00263 //TODO: leave this off first!! They overwrite stuff! 00264 /* TODO: Wei commented out the stuff below. To be checked why (Andras). 00265 d->setMaxRtrAdvInterval(OPP_Global::atod(getRequiredAttr(cfg, "MaxRtrAdvInterval"))); 00266 d->setMinRtrAdvInterval(OPP_Global::atod(getRequiredAttr(cfg, "MinRtrAdvInterval"))); 00267 d->setAdvManagedFlag(toBool(getRequiredAttr(cfg, "AdvManagedFlag"))); 00268 d->setAdvOtherConfigFlag(toBool(getRequiredAttr(cfg, "AdvOtherConfigFlag"))); 00269 d->setAdvLinkMTU(OPP_Global::atoul(getRequiredAttr(cfg, "AdvLinkMTU"))); 00270 d->setAdvReachableTime(OPP_Global::atoul(getRequiredAttr(cfg, "AdvReachableTime"))); 00271 d->setAdvRetransTimer(OPP_Global::atoul(getRequiredAttr(cfg, "AdvRetransTimer"))); 00272 d->setAdvCurHopLimit(OPP_Global::atoul(getRequiredAttr(cfg, "AdvCurHopLimit"))); 00273 d->setAdvDefaultLifetime(OPP_Global::atoul(getRequiredAttr(cfg, "AdvDefaultLifetime"))); 00274 ie->setMtu(OPP_Global::atoul(getRequiredAttr(cfg, "HostLinkMTU"))); 00275 d->setCurHopLimit(OPP_Global::atoul(getRequiredAttr(cfg, "HostCurHopLimit"))); 00276 d->setBaseReachableTime(OPP_Global::atoul(getRequiredAttr(cfg, "HostBaseReachableTime"))); 00277 d->setRetransTimer(OPP_Global::atoul(getRequiredAttr(cfg, "HostRetransTimer"))); 00278 d->setDupAddrDetectTransmits(OPP_Global::atoul(getRequiredAttr(cfg, "HostDupAddrDetectTransmits"))); 00279 */ 00280 00281 // parse prefixes (AdvPrefix elements; they should be inside an AdvPrefixList 00282 // element, but we don't check that) 00283 cXMLElementList prefixList = cfg->getElementsByTagName("AdvPrefix"); 00284 for (unsigned int i=0; i<prefixList.size(); i++) 00285 { 00286 cXMLElement *node = prefixList[i]; 00287 IPv6InterfaceData::AdvPrefix prefix; 00288 00289 // FIXME todo implement: advValidLifetime, advPreferredLifetime can 00290 // store (absolute) expiry time (if >0) or lifetime (delta) (if <0); 00291 // 0 should be treated as infinity 00292 int pfxLen; 00293 if (!prefix.prefix.tryParseAddrWithPrefix(node->getNodeValue(),pfxLen)) 00294 opp_error("element <%s> at %s: wrong IPv6Address/prefix syntax %s", 00295 node->getTagName(), node->getSourceLocation(), node->getNodeValue()); 00296 prefix.prefixLength = pfxLen; 00297 prefix.advValidLifetime = OPP_Global::atoul(getRequiredAttr(node, "AdvValidLifetime")); 00298 prefix.advOnLinkFlag = toBool(getRequiredAttr(node, "AdvOnLinkFlag")); 00299 prefix.advPreferredLifetime = OPP_Global::atoul(getRequiredAttr(node, "AdvPreferredLifetime")); 00300 prefix.advAutonomousFlag = toBool(getRequiredAttr(node, "AdvAutonomousFlag")); 00301 d->addAdvPrefix(prefix); 00302 } 00303 00304 // parse addresses 00305 cXMLElementList addrList = cfg->getChildrenByTagName("inetAddr"); 00306 for (unsigned int k=0; k<addrList.size(); k++) 00307 { 00308 cXMLElement *node = addrList[k]; 00309 IPv6Address address(node->getNodeValue()); 00310 //We can now decide if the address is tentative or not. 00311 d->assignAddress(address, toBool(getRequiredAttr(node, "tentative")), 0, 0); // set up with infinite lifetimes 00312 } 00313 }
|
|
Performs longest prefix match in the routing table and returns the resulting route, or NULL if there was no match. 00379 { 00380 Enter_Method("doLongestPrefixMatch(%s)", dest.str().c_str()); 00381 00382 // we'll just stop at the first match, because the table is sorted 00383 // by prefix lengths and metric (see addRoute()) 00384 for (RouteList::iterator it=routeList.begin(); it!=routeList.end(); it++) 00385 { 00386 if (dest.matches((*it)->destPrefix(),(*it)->prefixLength())) 00387 { 00388 // FIXME proofread this code, iterator invalidation-wise, etc 00389 bool entryExpired = false; 00390 if (simTime() > (*it)->expiryTime() && (*it)->expiryTime() != 0)//since 0 represents infinity. 00391 { 00392 EV << "Expired prefix detected!!" << endl; 00393 removeOnLinkPrefix((*it)->destPrefix(), (*it)->prefixLength()); 00394 entryExpired = true; 00395 } 00396 if (entryExpired == false) return *it; 00397 } 00398 } 00399 // FIXME todo: if we selected an expired route, throw it out and select again! 00400 return NULL; 00401 }
|
|
Raises an error. 00174 {
00175 opp_error("This module doesn't process messages");
00176 }
|
|
00085 { 00086 if (stage==1) 00087 { 00088 ift = InterfaceTableAccess().get(); 00089 00090 WATCH_PTRVECTOR(routeList); 00091 WATCH_MAP(destCache); // FIXME commented out for now 00092 isrouter = par("isRouter"); 00093 WATCH(isrouter); 00094 00095 // add IPv6InterfaceData to interfaces 00096 for (int i=0; i<ift->numInterfaces(); i++) 00097 { 00098 InterfaceEntry *ie = ift->interfaceAt(i); 00099 configureInterfaceForIPv6(ie); 00100 } 00101 00102 parseXMLConfigFile(); 00103 00104 // skip hosts 00105 if (isrouter) 00106 { 00107 // add globally routable prefixes to routing table 00108 for (int x = 0; x < ift->numInterfaces(); x++) 00109 { 00110 InterfaceEntry *ie = ift->interfaceAt(x); 00111 00112 if (ie->isLoopback()) 00113 continue; 00114 00115 for (int y = 0; y < ie->ipv6()->numAdvPrefixes(); y++) 00116 if (ie->ipv6()->advPrefix(y).prefix.isGlobal()) 00117 addOrUpdateOwnAdvPrefix(ie->ipv6()->advPrefix(y).prefix, 00118 ie->ipv6()->advPrefix(y).prefixLength, 00119 x, 0); 00120 } 00121 } 00122 } 00123 else if (stage==4) 00124 { 00125 // configurator adds routes only in stage==3 00126 updateDisplayString(); 00127 } 00128 }
|
|
Returns an interface given by its address. Returns NULL if not found. 00316 { 00317 Enter_Method("interfaceByAddress(%s)=?", addr.str().c_str()); 00318 00319 if (addr.isUnspecified()) 00320 return NULL; 00321 for (int i=0; i<ift->numInterfaces(); ++i) 00322 { 00323 InterfaceEntry *ie = ift->interfaceAt(i); 00324 if (ie->ipv6()->hasAddress(addr)) 00325 return ie; 00326 } 00327 return NULL; 00328 }
|
|
Checks if the given prefix already exists in the routing table (prefix list) 00404 { 00405 for (RouteList::iterator it=routeList.begin(); it!=routeList.end(); it++) 00406 if (prefix.matches((*it)->destPrefix(),128)) 00407 return true; 00408 return false; 00409 }
|
|
IP forwarding on/off 00171 {return isrouter;}
|
|
Checks if the address is one of the host's addresses, i.e. assigned to one of its interfaces (tentatively or not). 00331 { 00332 Enter_Method("localDeliver(%s) y/n", dest.str().c_str()); 00333 00334 // first, check if we have an interface with this address 00335 for (int i=0; i<ift->numInterfaces(); i++) 00336 { 00337 InterfaceEntry *ie = ift->interfaceAt(i); 00338 if (ie->ipv6()->hasAddress(dest)) 00339 return true; 00340 } 00341 00342 // then check for special, preassigned multicast addresses 00343 // (these addresses occur more rarely than specific interface addresses, 00344 // that's why we check for them last) 00345 00346 if (dest==IPv6Address::ALL_NODES_1 || dest==IPv6Address::ALL_NODES_2) 00347 return true; 00348 if (isRouter() && (dest==IPv6Address::ALL_ROUTERS_1 || dest==IPv6Address::ALL_ROUTERS_2 || dest==IPv6Address::ALL_ROUTERS_5)) 00349 return true; 00350 00351 // check for solicited-node multicast address 00352 if (dest.matches(IPv6Address::SOLICITED_NODE_PREFIX, 104)) 00353 { 00354 for (int i=0; i<ift->numInterfaces(); i++) 00355 { 00356 InterfaceEntry *ie = ift->interfaceAt(i); 00357 if (ie->ipv6()->matchesSolicitedNodeMulticastAddress(dest)) 00358 return true; 00359 } 00360 } 00361 return false; 00362 }
|
|
Looks up the given destination address in the Destination Cache, then returns the next-hop address and the interface in the outInterfaceId variable. If the destination is not in the cache, outInterfaceId is set to -1 and the unspecified address is returned. The caller should check for interfaceId==-1, because unspecified address is also returned if the link layer doesn't use addresses at all (e.g. PPP). NOTE: outInterfaceId is an OUTPUT parameter -- its initial value is ignored, and the lookupDestCache() sets it to the correct value instead. 00365 { 00366 Enter_Method("lookupDestCache(%s)", dest.str().c_str()); 00367 00368 DestCache::iterator it = destCache.find(dest); 00369 if (it == destCache.end()) 00370 { 00371 outInterfaceId = -1; 00372 return IPv6Address::UNSPECIFIED_ADDRESS; 00373 } 00374 outInterfaceId = it->second.interfaceId; 00375 return it->second.nextHopAddr; 00376 }
|
|
00150 {return 5;}
|
|
Return the number of routes. 00600 { 00601 return routeList.size(); 00602 }
|
|
00131 { 00132 // TODO to be revised by Andras 00133 // configure interfaces from XML config file 00134 cXMLElement *config = par("routingTableFile"); 00135 for (cXMLElement *child=config->getFirstChild(); child; child = child->getNextSibling()) 00136 { 00137 //std::cout << "configuring interfaces from XML file." << endl; 00138 //std::cout << "selected element is: " << child->getTagName() << endl; 00139 // we ensure that the selected element is local. 00140 if (opp_strcmp(child->getTagName(),"local")!=0) continue; 00141 //ensure that this is the right parent module we are configuring. 00142 if (opp_strcmp(child->getAttribute("node"),parentModule()->fullName())!=0) 00143 continue; 00144 //Go one level deeper. 00145 //child = child->getFirstChild(); 00146 for (cXMLElement *ifTag=child->getFirstChild(); ifTag; ifTag = ifTag->getNextSibling()) 00147 { 00148 //The next tag should be "interface". 00149 if (opp_strcmp(ifTag->getTagName(),"interface")!=0) 00150 continue; 00151 //std::cout << "Getting attribute: name" << endl; 00152 const char *ifname = ifTag->getAttribute("name"); 00153 if (!ifname) 00154 error("<interface> without name attribute at %s", child->getSourceLocation()); 00155 InterfaceEntry *ie = ift->interfaceByName(ifname); 00156 if (!ie) 00157 error("no interface named %s was registered, %s", ifname, child->getSourceLocation()); 00158 configureInterfaceFromXML(ie, ifTag); 00159 } 00160 } 00161 }
|
|
Discard all entries in destination cache 00421 { 00422 destCache.clear(); 00423 updateDisplayString(); 00424 }
|
|
Discard all entries in destination cache where next hop is the given address on the given interface. This is typically called when a router becomes unreachable, and all destinations going via that router have to go though router selection again. 00427 { 00428 for (DestCache::iterator it=destCache.begin(); it!=destCache.end(); ) 00429 { 00430 if (it->second.interfaceId==interfaceId && it->second.nextHopAddr==nextHopAddr) 00431 { 00432 // move the iterator past this element before removing it 00433 DestCache::iterator oldIt = it++; 00434 destCache.erase(oldIt); 00435 } 00436 else 00437 { 00438 it++; 00439 } 00440 } 00441 00442 updateDisplayString(); 00443 }
|
|
Remove an on-link prefix. To be called when the prefix gets advertised with zero lifetime, or to purge an expired prefix. NOTE: This method does NOT remove the matching addresses from the InterfaceTable (see IPv6InterfaceData); that has to be done separately. 00518 { 00519 // scan the routing table for this prefix and remove it 00520 for (RouteList::iterator it=routeList.begin(); it!=routeList.end(); it++) 00521 { 00522 if ((*it)->src()==IPv6Route::FROM_RA && (*it)->destPrefix()==destPrefix && (*it)->prefixLength()==prefixLength) 00523 { 00524 routeList.erase(it); 00525 return; // there can be only one such route, addOrUpdateOnLinkPrefix() guarantees that 00526 } 00527 } 00528 00529 updateDisplayString(); 00530 }
|
|
Deletes the given route from the route table. 00591 { 00592 RouteList::iterator it = std::find(routeList.begin(), routeList.end(), route); 00593 ASSERT(it!=routeList.end()); 00594 routeList.erase(it); 00595 00596 updateDisplayString(); 00597 }
|
|
Return the ith route.
|
|
00570 { 00571 // helper for sort() in addRoute(). We want routes with longer 00572 // prefixes to be at front, so we compare them as "less". 00573 // For metric, a smaller value is better (we report that as "less"). 00574 if (a->prefixLength()!=b->prefixLength()) 00575 return a->prefixLength() > b->prefixLength(); 00576 return a->metric() < b->metric(); 00577 }
|
|
Add or update a destination cache entry. 00412 { 00413 // FIXME this performs 2 lookups -- optimize to do only one 00414 destCache[dest].nextHopAddr = nextHopAddr; 00415 destCache[dest].interfaceId = interfaceId; 00416 00417 updateDisplayString(); 00418 }
|
|
00164 { 00165 if (!ev.isGUI()) 00166 return; 00167 00168 char buf[80]; 00169 sprintf(buf, "%d routes\n%d destcache entries", numRoutes(), destCache.size()); 00170 displayString().setTagArg("t",0,buf); 00171 }
|
|
00069 { 00070 os << "if=" << e.interfaceId << " " << e.nextHopAddr; //FIXME try printing interface name 00071 return os; 00072 };
|
|
|
|
|
|
|
|
|