mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-01-20 03:36:40 +00:00
Whole bunch of stuff: netconf, bug fixes, tweaks to ping and firewall opener timing code.
This commit is contained in:
parent
c9c63074bb
commit
e73c4cb68b
@ -114,7 +114,7 @@ EthernetTap::EthernetTap(
|
|||||||
|
|
||||||
_fd = ::open("/dev/net/tun",O_RDWR);
|
_fd = ::open("/dev/net/tun",O_RDWR);
|
||||||
if (_fd <= 0)
|
if (_fd <= 0)
|
||||||
throw std::runtime_error("could not open TUN/TAP device");
|
throw std::runtime_error(std::string("could not open TUN/TAP device: ") + strerror(errno));
|
||||||
|
|
||||||
struct ifreq ifr;
|
struct ifreq ifr;
|
||||||
memset(&ifr,0,sizeof(ifr));
|
memset(&ifr,0,sizeof(ifr));
|
||||||
|
@ -68,7 +68,7 @@ class Multicaster
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* 256-bit simple bloom filter included with multicast frame packets
|
* Simple bit field bloom filter included with multicast frame packets
|
||||||
*/
|
*/
|
||||||
typedef BloomFilter<ZT_PROTO_VERB_MULTICAST_FRAME_BLOOM_FILTER_SIZE_BITS> MulticastBloomFilter;
|
typedef BloomFilter<ZT_PROTO_VERB_MULTICAST_FRAME_BLOOM_FILTER_SIZE_BITS> MulticastBloomFilter;
|
||||||
|
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
* LLC. Start here: http://www.zerotier.com/
|
* LLC. Start here: http://www.zerotier.com/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
@ -35,6 +37,7 @@
|
|||||||
#include "Network.hpp"
|
#include "Network.hpp"
|
||||||
#include "Switch.hpp"
|
#include "Switch.hpp"
|
||||||
#include "Packet.hpp"
|
#include "Packet.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace ZeroTier {
|
namespace ZeroTier {
|
||||||
|
|
||||||
@ -106,14 +109,44 @@ Network::Network(const RuntimeEnvironment *renv,uint64_t id)
|
|||||||
_r(renv),
|
_r(renv),
|
||||||
_tap(renv,renv->identity.address().toMAC(),ZT_IF_MTU,&_CBhandleTapData,this),
|
_tap(renv,renv->identity.address().toMAC(),ZT_IF_MTU,&_CBhandleTapData,this),
|
||||||
_id(id),
|
_id(id),
|
||||||
_lastConfigUpdate(0)
|
_lastConfigUpdate(0),
|
||||||
|
_destroyOnDelete(false)
|
||||||
{
|
{
|
||||||
if (controller() == _r->identity.address())
|
if (controller() == _r->identity.address())
|
||||||
throw std::runtime_error("configuration error: cannot add a network for which I am the netconf master");
|
throw std::runtime_error("configuration error: cannot add a network for which I am the netconf master");
|
||||||
|
|
||||||
|
std::string confPath(_r->homePath + ZT_PATH_SEPARATOR_S + "networks.d" + ZT_PATH_SEPARATOR_S + toString() + ".conf");
|
||||||
|
std::string confs;
|
||||||
|
if (Utils::readFile(confPath.c_str(),confs)) {
|
||||||
|
try {
|
||||||
|
if (confs.length()) {
|
||||||
|
Config conf(confs);
|
||||||
|
if (conf.containsAllFields())
|
||||||
|
setConfiguration(Config(conf));
|
||||||
|
}
|
||||||
|
} catch ( ... ) {} // ignore invalid config on disk, we will re-request
|
||||||
|
} else {
|
||||||
|
// If the conf file isn't present, "touch" it so we'll remember
|
||||||
|
// the existence of this network.
|
||||||
|
FILE *tmp = fopen(confPath.c_str(),"w");
|
||||||
|
if (tmp)
|
||||||
|
fclose(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
Network::~Network()
|
Network::~Network()
|
||||||
{
|
{
|
||||||
|
if (_destroyOnDelete) {
|
||||||
|
std::string confPath(_r->homePath + ZT_PATH_SEPARATOR_S + "networks.d" + ZT_PATH_SEPARATOR_S + toString() + ".conf");
|
||||||
|
std::string mcdbPath(_r->homePath + ZT_PATH_SEPARATOR_S + "networks.d" + ZT_PATH_SEPARATOR_S + toString() + ".mcerts");
|
||||||
|
unlink(confPath.c_str());
|
||||||
|
unlink(mcdbPath.c_str());
|
||||||
|
} else {
|
||||||
|
// Causes flush of membership certs to disk
|
||||||
|
clean();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Network::setConfiguration(const Network::Config &conf)
|
void Network::setConfiguration(const Network::Config &conf)
|
||||||
@ -124,6 +157,11 @@ void Network::setConfiguration(const Network::Config &conf)
|
|||||||
_configuration = conf;
|
_configuration = conf;
|
||||||
_myCertificate = conf.certificateOfMembership();
|
_myCertificate = conf.certificateOfMembership();
|
||||||
_lastConfigUpdate = Utils::now();
|
_lastConfigUpdate = Utils::now();
|
||||||
|
|
||||||
|
std::string confPath(_r->homePath + ZT_PATH_SEPARATOR_S + "networks.d" + ZT_PATH_SEPARATOR_S + toString() + ".conf");
|
||||||
|
if (!Utils::writeFile(confPath.c_str(),conf.toString())) {
|
||||||
|
LOG("error: unable to write network configuration file at: %s",confPath.c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +174,17 @@ void Network::requestConfiguration()
|
|||||||
TRACE("requesting netconf for network %.16llx from netconf master %s",(unsigned long long)_id,controller().toString().c_str());
|
TRACE("requesting netconf for network %.16llx from netconf master %s",(unsigned long long)_id,controller().toString().c_str());
|
||||||
Packet outp(controller(),_r->identity.address(),Packet::VERB_NETWORK_CONFIG_REQUEST);
|
Packet outp(controller(),_r->identity.address(),Packet::VERB_NETWORK_CONFIG_REQUEST);
|
||||||
outp.append((uint64_t)_id);
|
outp.append((uint64_t)_id);
|
||||||
|
outp.append((uint16_t)0); // no meta-data
|
||||||
_r->sw->send(outp,true);
|
_r->sw->send(outp,true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Network::addMembershipCertificate(const Address &peer,const Certificate &cert)
|
||||||
|
{
|
||||||
|
Mutex::Lock _l(_lock);
|
||||||
|
if (!_configuration.isOpen())
|
||||||
|
_membershipCertificates[peer] = cert;
|
||||||
|
}
|
||||||
|
|
||||||
bool Network::isAllowed(const Address &peer) const
|
bool Network::isAllowed(const Address &peer) const
|
||||||
{
|
{
|
||||||
// Exceptions can occur if we do not yet have *our* configuration.
|
// Exceptions can occur if we do not yet have *our* configuration.
|
||||||
@ -164,10 +210,39 @@ void Network::clean()
|
|||||||
if (_configuration.isOpen())
|
if (_configuration.isOpen())
|
||||||
_membershipCertificates.clear();
|
_membershipCertificates.clear();
|
||||||
else {
|
else {
|
||||||
|
std::string mcdbPath(_r->homePath + ZT_PATH_SEPARATOR_S + "networks.d" + ZT_PATH_SEPARATOR_S + toString() + ".mcerts");
|
||||||
|
FILE *mcdb = fopen(mcdbPath.c_str(),"wb");
|
||||||
|
bool writeError = false;
|
||||||
|
if (!mcdb) {
|
||||||
|
LOG("error: unable to open membership cert database at: %s",mcdbPath.c_str());
|
||||||
|
} else {
|
||||||
|
if ((writeError)||(fwrite("MCDB0",5,1,mcdb) != 1)) // version
|
||||||
|
writeError = true;
|
||||||
|
}
|
||||||
|
|
||||||
for(std::map<Address,Certificate>::iterator i=(_membershipCertificates.begin());i!=_membershipCertificates.end();) {
|
for(std::map<Address,Certificate>::iterator i=(_membershipCertificates.begin());i!=_membershipCertificates.end();) {
|
||||||
if (_myCertificate.qualifyMembership(i->second))
|
if (_myCertificate.qualifyMembership(i->second)) {
|
||||||
|
if ((!writeError)&&(mcdb)) {
|
||||||
|
char tmp[ZT_ADDRESS_LENGTH];
|
||||||
|
i->first.copyTo(tmp,ZT_ADDRESS_LENGTH);
|
||||||
|
if ((writeError)||(fwrite(tmp,ZT_ADDRESS_LENGTH,1,mcdb) != 1))
|
||||||
|
writeError = true;
|
||||||
|
std::string c(i->second.toString());
|
||||||
|
uint32_t cl = Utils::hton((uint32_t)c.length());
|
||||||
|
if ((writeError)||(fwrite(&cl,sizeof(cl),1,mcdb) != 1))
|
||||||
|
writeError = true;
|
||||||
|
if ((writeError)||(fwrite(c.data(),c.length(),1,mcdb) != 1))
|
||||||
|
writeError = true;
|
||||||
|
}
|
||||||
++i;
|
++i;
|
||||||
else _membershipCertificates.erase(i++);
|
} else _membershipCertificates.erase(i++);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mcdb)
|
||||||
|
fclose(mcdb);
|
||||||
|
if (writeError) {
|
||||||
|
unlink(mcdbPath.c_str());
|
||||||
|
LOG("error: unable to write to membership cert database at: %s",mcdbPath.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,6 +208,11 @@ public:
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool containsAllFields() const
|
||||||
|
{
|
||||||
|
return (contains("nwid")&&contains("peer"));
|
||||||
|
}
|
||||||
|
|
||||||
inline std::string toString() const
|
inline std::string toString() const
|
||||||
{
|
{
|
||||||
return Dictionary::toString();
|
return Dictionary::toString();
|
||||||
@ -241,7 +246,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
inline bool isOpen() const
|
inline bool isOpen() const
|
||||||
{
|
{
|
||||||
return (get("isOpen") == "1");
|
return (get("isOpen","0") == "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -267,6 +272,14 @@ private:
|
|||||||
|
|
||||||
~Network();
|
~Network();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Causes all persistent disk presence to be erased on delete
|
||||||
|
*/
|
||||||
|
inline void destroyOnDelete()
|
||||||
|
{
|
||||||
|
_destroyOnDelete = true;
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @return Network ID
|
* @return Network ID
|
||||||
@ -350,16 +363,16 @@ public:
|
|||||||
* @param peer Peer that owns certificate
|
* @param peer Peer that owns certificate
|
||||||
* @param cert Certificate itself
|
* @param cert Certificate itself
|
||||||
*/
|
*/
|
||||||
inline void addMembershipCertificate(const Address &peer,const Certificate &cert)
|
void addMembershipCertificate(const Address &peer,const Certificate &cert);
|
||||||
{
|
|
||||||
Mutex::Lock _l(_lock);
|
|
||||||
_membershipCertificates[peer] = cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param peer Peer address to check
|
||||||
|
* @return True if peer is allowed to communicate on this network
|
||||||
|
*/
|
||||||
bool isAllowed(const Address &peer) const;
|
bool isAllowed(const Address &peer) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform periodic database cleaning such as removing expired membership certificates
|
* Perform cleanup and possibly save state
|
||||||
*/
|
*/
|
||||||
void clean();
|
void clean();
|
||||||
|
|
||||||
@ -377,16 +390,20 @@ private:
|
|||||||
|
|
||||||
const RuntimeEnvironment *_r;
|
const RuntimeEnvironment *_r;
|
||||||
|
|
||||||
|
// Tap and tap multicast memberships
|
||||||
EthernetTap _tap;
|
EthernetTap _tap;
|
||||||
|
|
||||||
std::set<MulticastGroup> _multicastGroups;
|
std::set<MulticastGroup> _multicastGroups;
|
||||||
|
|
||||||
|
// Membership certificates supplied by peers
|
||||||
std::map<Address,Certificate> _membershipCertificates;
|
std::map<Address,Certificate> _membershipCertificates;
|
||||||
|
|
||||||
|
// Configuration from network master node
|
||||||
Config _configuration;
|
Config _configuration;
|
||||||
Certificate _myCertificate;
|
Certificate _myCertificate;
|
||||||
|
|
||||||
uint64_t _id;
|
uint64_t _id;
|
||||||
volatile uint64_t _lastConfigUpdate;
|
volatile uint64_t _lastConfigUpdate;
|
||||||
|
volatile bool _destroyOnDelete;
|
||||||
|
|
||||||
Mutex _lock;
|
Mutex _lock;
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "Condition.hpp"
|
#include "Condition.hpp"
|
||||||
@ -340,6 +341,9 @@ Node::ReasonForTermination Node::run()
|
|||||||
unlink((_r->homePath + ZT_PATH_SEPARATOR_S + "status").c_str());
|
unlink((_r->homePath + ZT_PATH_SEPARATOR_S + "status").c_str());
|
||||||
unlink((_r->homePath + ZT_PATH_SEPARATOR_S + "thisdeviceismine").c_str());
|
unlink((_r->homePath + ZT_PATH_SEPARATOR_S + "thisdeviceismine").c_str());
|
||||||
|
|
||||||
|
// Make sure networks.d exists
|
||||||
|
mkdir((_r->homePath + ZT_PATH_SEPARATOR_S + "networks.d").c_str(),0700);
|
||||||
|
|
||||||
// Load or generate config authentication secret
|
// Load or generate config authentication secret
|
||||||
std::string configAuthTokenPath(_r->homePath + ZT_PATH_SEPARATOR_S + "authtoken.secret");
|
std::string configAuthTokenPath(_r->homePath + ZT_PATH_SEPARATOR_S + "authtoken.secret");
|
||||||
std::string configAuthToken;
|
std::string configAuthToken;
|
||||||
@ -504,7 +508,6 @@ Node::ReasonForTermination Node::run()
|
|||||||
_r->topology->eachPeer(Topology::CollectPeersWithDirectPath(needPing));
|
_r->topology->eachPeer(Topology::CollectPeersWithDirectPath(needPing));
|
||||||
} else {
|
} else {
|
||||||
_r->topology->eachPeer(Topology::CollectPeersThatNeedPing(needPing));
|
_r->topology->eachPeer(Topology::CollectPeersThatNeedPing(needPing));
|
||||||
_r->topology->eachPeer(Topology::CollectPeersThatNeedFirewallOpener(needFirewallOpener));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for(std::vector< SharedPtr<Peer> >::iterator p(needPing.begin());p!=needPing.end();++p) {
|
for(std::vector< SharedPtr<Peer> >::iterator p(needPing.begin());p!=needPing.end();++p) {
|
||||||
@ -517,6 +520,7 @@ Node::ReasonForTermination Node::run()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_r->topology->eachPeer(Topology::CollectPeersThatNeedFirewallOpener(needFirewallOpener));
|
||||||
for(std::vector< SharedPtr<Peer> >::iterator p(needFirewallOpener.begin());p!=needFirewallOpener.end();++p) {
|
for(std::vector< SharedPtr<Peer> >::iterator p(needFirewallOpener.begin());p!=needFirewallOpener.end();++p) {
|
||||||
try {
|
try {
|
||||||
(*p)->sendFirewallOpener(_r,now);
|
(*p)->sendFirewallOpener(_r,now);
|
||||||
@ -537,7 +541,7 @@ Node::ReasonForTermination Node::run()
|
|||||||
if ((now - lastClean) >= ZT_DB_CLEAN_PERIOD) {
|
if ((now - lastClean) >= ZT_DB_CLEAN_PERIOD) {
|
||||||
lastClean = now;
|
lastClean = now;
|
||||||
_r->topology->clean();
|
_r->topology->clean();
|
||||||
_r->nc->cleanAllNetworks();
|
_r->nc->clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -72,7 +72,7 @@ void NodeConfig::whackAllTaps()
|
|||||||
n->second->tap().whack();
|
n->second->tap().whack();
|
||||||
}
|
}
|
||||||
|
|
||||||
void NodeConfig::cleanAllNetworks()
|
void NodeConfig::clean()
|
||||||
{
|
{
|
||||||
Mutex::Lock _l(_networks_m);
|
Mutex::Lock _l(_networks_m);
|
||||||
for(std::map< uint64_t,SharedPtr<Network> >::const_iterator n(_networks.begin());n!=_networks.end();++n)
|
for(std::map< uint64_t,SharedPtr<Network> >::const_iterator n(_networks.begin());n!=_networks.end();++n)
|
||||||
@ -145,9 +145,39 @@ std::vector<std::string> NodeConfig::execute(const char *command)
|
|||||||
tmp.c_str());
|
tmp.c_str());
|
||||||
}
|
}
|
||||||
} else if (cmd[0] == "join") {
|
} else if (cmd[0] == "join") {
|
||||||
_P("404 join Not implemented yet.");
|
if (cmd.size() > 1) {
|
||||||
|
uint64_t nwid = strtoull(cmd[1].c_str(),(char **)0,16);
|
||||||
|
if (nwid > 0) {
|
||||||
|
Mutex::Lock _l(_networks_m);
|
||||||
|
try {
|
||||||
|
SharedPtr<Network> nw(new Network(_r,nwid));
|
||||||
|
_networks[nwid] = nw;
|
||||||
|
_P("200 join %.16llx OK",(unsigned long long)nwid);
|
||||||
|
} catch (std::exception &exc) {
|
||||||
|
_P("500 join %.16llx ERROR: %s",(unsigned long long)nwid,exc.what());
|
||||||
|
} catch ( ... ) {
|
||||||
|
_P("500 join %.16llx ERROR: (unknown exception)",(unsigned long long)nwid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_P("400 join requires a network ID (>0) in hexadecimal format");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_P("400 join requires a network ID (>0) in hexadecimal format");
|
||||||
|
}
|
||||||
} else if (cmd[0] == "leave") {
|
} else if (cmd[0] == "leave") {
|
||||||
_P("404 leave Not implemented yet.");
|
if (cmd.size() > 1) {
|
||||||
|
Mutex::Lock _l(_networks_m);
|
||||||
|
uint64_t nwid = strtoull(cmd[1].c_str(),(char **)0,16);
|
||||||
|
std::map< uint64_t,SharedPtr<Network> >::iterator nw(_networks.find(nwid));
|
||||||
|
if (nw == _networks.end()) {
|
||||||
|
_P("404 leave %.16llx ERROR: not a member of that network",(unsigned long long)nwid);
|
||||||
|
} else {
|
||||||
|
nw->second->destroyOnDelete();
|
||||||
|
_networks.erase(nw);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_P("400 leave requires a network ID (>0) in hexadecimal format");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_P("404 %s No such command. Use 'help' for help.",cmd[0].c_str());
|
_P("404 %s No such command. Use 'help' for help.",cmd[0].c_str());
|
||||||
}
|
}
|
||||||
|
@ -95,9 +95,9 @@ public:
|
|||||||
void whackAllTaps();
|
void whackAllTaps();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call clean() on all networks
|
* Perform cleanup and possibly update saved state
|
||||||
*/
|
*/
|
||||||
void cleanAllNetworks();
|
void clean();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param nwid Network ID
|
* @param nwid Network ID
|
||||||
|
@ -610,6 +610,7 @@ bool PacketDecoder::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *_r,const
|
|||||||
char tmp[128];
|
char tmp[128];
|
||||||
try {
|
try {
|
||||||
uint64_t nwid = at<uint64_t>(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST_IDX_NETWORK_ID);
|
uint64_t nwid = at<uint64_t>(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST_IDX_NETWORK_ID);
|
||||||
|
TRACE("NETWORK_CONFIG_REQUEST for %.16llx from %s",(unsigned long long)nwid,source().toString().c_str());
|
||||||
#ifndef __WINDOWS__
|
#ifndef __WINDOWS__
|
||||||
if (_r->netconfService) {
|
if (_r->netconfService) {
|
||||||
unsigned int dictLen = at<uint16_t>(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST_IDX_DICT_LEN);
|
unsigned int dictLen = at<uint16_t>(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST_IDX_DICT_LEN);
|
||||||
|
@ -200,7 +200,7 @@ public:
|
|||||||
|
|
||||||
inline void operator()(Topology &t,const SharedPtr<Peer> &p)
|
inline void operator()(Topology &t,const SharedPtr<Peer> &p)
|
||||||
{
|
{
|
||||||
if ((p->hasDirectPath())&&((_now - p->lastFirewallOpener()) >= ZT_FIREWALL_OPENER_DELAY))
|
if ((p->hasDirectPath())&&((_now - std::max(p->lastFirewallOpener(),p->lastDirectSend())) >= ZT_FIREWALL_OPENER_DELAY))
|
||||||
_v.push_back(p);
|
_v.push_back(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ public:
|
|||||||
|
|
||||||
inline void operator()(Topology &t,const SharedPtr<Peer> &p)
|
inline void operator()(Topology &t,const SharedPtr<Peer> &p)
|
||||||
{
|
{
|
||||||
if (((p->hasActiveDirectPath(_now))||(t.isSupernode(p->address())))&&((_now - p->lastDirectSend()) >= ZT_PEER_DIRECT_PING_DELAY))
|
if ( ((t.isSupernode(p->address()))&&((_now - p->lastDirectReceive()) >= ZT_PEER_DIRECT_PING_DELAY)) || ((p->hasActiveDirectPath(_now))&&((_now - p->lastDirectSend()) >= ZT_PEER_DIRECT_PING_DELAY)) )
|
||||||
_v.push_back(p);
|
_v.push_back(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user