Xndp.h
#pragma once
#include <frp/IDisposable.h>
#include <frp/threading/Hosting.h>
#include <frp/net/IPEndPoint.h>
#include <frp/configuration/MappingType.h>
namespace frp {
namespace discover {
/* X Network Discovery Protocol. */
class Xndp : public IDisposable { /* Apply to P2P(peer-to-peer) is based on NAT/UDP put holes.*/
#pragma pack(push,1)
struct XndpPacket {
Byte kf;
UInt16 len; /* Packet size. */
UInt16 checksum; /* Check sum. */
UInt32 id;
Byte cmd;
};
struct XndpRegisterEntryRequestPacket : XndpPacket {
Byte type; /* frp::configuration::MappingType */
UInt16 port;
};
struct XndpReisterReplyPacket : XndpPacket {
Byte err;
};
struct XndpQueryEntryReplyPacket : XndpReisterReplyPacket {
Byte type; /* frp::configuration::MappingType */
UInt16 port;
Byte af;
UInt16 dst_port;
Byte dst_addr[16];
};
typedef XndpRegisterEntryRequestPacket XndpQueryEntryRequestPacket;
#pragma pack(pop)
enum {
XNDP_KF = 0x7E,
XNDP_REGST_ENTRY = 0x2A, /* Registry entry. */
XNDP_QUERY_ENTRY = 0x2E, /* Query entry. */
XNDP_PUSH_MESSAGE = 0x2C,
XNDP_ENTRY_TIMEOUT = 10000,
XNDP_CALLER_TIMEOUT = 3000,
XNDP_ERROR_SUCCESS = 0,
XNDP_ERROR_INVALID_ENTRY_TYPE = 1,
XNDP_ERROR_INVALID_ENTRY_PORT = 2,
XNDP_ERROR_NOT_FOUND_ENTRY = 3,
};
typedef boost::asio::ip::udp::endpoint XNDP_ENDPOINT;
struct XndpEntry {
XNDP_ENDPOINT destinationEP; /* Mapped to a public address. */
UInt64 last; /* Last activity times. */
Byte type;
UInt16 port;
};
/* s mode. */
typedef std::shared_ptr<XndpEntry> XndpEntryPtr;
typedef std::unordered_map<UInt16, XndpEntryPtr> XndpEntry2LevelTable;
typedef std::unordered_map<Byte, XndpEntry2LevelTable> XndpEntry1LevelTable;
public:
typedef std::function<void(
UInt32 id,
Byte* buffer,
int length,
boost::asio::ip::udp::endpoint& remoteEP)> MessageEventHandler;
MessageEventHandler MessageEvent;
public:
Xndp(const std::shared_ptr<frp::threading::Hosting>& hosting) noexcept;
Xndp(const std::shared_ptr<frp::threading::Hosting>& hosting, int bindport) noexcept;
public:
boost::asio::ip::udp::endpoint GetLocalEndPoint() noexcept;
boost::asio::ip::udp::socket& GetSocket() noexcept;
std::shared_ptr<boost::asio::io_context> GetContext() noexcept;
std::shared_ptr<frp::threading::Hosting> GetHosting() noexcept;
public:
virtual bool IsAvailable() noexcept;
virtual bool Open() noexcept;
void Close() noexcept;
virtual void Dispose() noexcept override;
public:
typedef std::function<void(bool)> RegisterAsyncCallback;
typedef std::function<void(bool, boost::asio::ip::udp::endpoint&)> QueryAsyncCallback;
virtual bool RegisterAsync(
frp::configuration::MappingType mapping_type,
int mapping_port,
RegisterAsyncCallback callback,
const boost::asio::ip::udp::endpoint& destinationEP) noexcept;
virtual bool QueryAsync(
frp::configuration::MappingType mapping_type,
int mapping_port,
QueryAsyncCallback callback,
const boost::asio::ip::udp::endpoint& destinationEP) noexcept;
virtual bool PushAsync(
UInt32 id,
const void* message,
const int message_size,
const boost::asio::ip::udp::endpoint& destinationEP) noexcept;
static boost::asio::ip::udp::endpoint ToEndPoint(const std::string host, int port) noexcept;
protected:
virtual void Timeout(UInt64 now) noexcept;
private:
bool SetTimeout() noexcept;
int PacketSerializeMini(XndpPacket* packet) noexcept;
bool OnXndpProc() noexcept;
bool OnRegisterEntryServer(XndpPacket* packet, const boost::asio::ip::udp::endpoint& remoteEP) noexcept;
bool OnQueryEntryServer(XndpPacket* packet, const boost::asio::ip::udp::endpoint& remoteEP) noexcept;
bool ReplyToDestination(XndpPacket* packet, const boost::asio::ip::udp::endpoint& remoteEP) noexcept;
void ProcessEntryTimeout(UInt64 now) noexcept;
private:
/* c mode. */
struct XndpCallerContext { /* rpc, stop wait ARQ. */
std::shared_ptr<Byte> message;
int message_size;
int retry_count;
UInt64 initial_time;
XNDP_ENDPOINT destination;
UInt32 id;
QueryAsyncCallback query_ac;
RegisterAsyncCallback register_ac;
};
typedef std::unordered_map<UInt32, XndpCallerContext> XndpCallerContextTable;
bool OnRegisterEntryClient(XndpPacket* packet) noexcept;
bool OnQueryEntryClient(XndpPacket* packet) noexcept;
bool PopCallerContext(UInt32 id, XndpCallerContext& context) noexcept;
void ProcessCallerTimeout(UInt64 now) noexcept;
template<typename TCallback>
bool RequireArgument(
frp::configuration::MappingType mapping_type,
int mapping_port,
TCallback& callback,
const boost::asio::ip::udp::endpoint& destinationEP) noexcept;
UInt32 NewId() noexcept;
template<typename TXndpRequestPacket>
bool AddXndpCaller(
Byte command,
Byte mapping_type,
int mapping_port,
const QueryAsyncCallback& qac,
const RegisterAsyncCallback& rac,
const boost::asio::ip::udp::endpoint& destinationEP) noexcept;
void OnTimeoutError(XndpCallerContext& context) noexcept;
private:
std::atomic<bool> disposed_;
std::atomic<UInt32> aid_;
int bindport_;
std::shared_ptr<frp::threading::Hosting> hosting_;
std::shared_ptr<boost::asio::io_context> context_;
std::shared_ptr<Byte> buffer_;
boost::asio::ip::udp::socket socket_;
boost::asio::ip::udp::endpoint localEP_;
boost::asio::ip::udp::endpoint endpoint_;
boost::asio::deadline_timer timeout_;
XndpEntry1LevelTable entries_;
XndpCallerContextTable callers_;
};
}
}
Xndp.cpp
#include <frp/discover/Xndp.h>
#include <frp/messages/checksum.h>
#include <frp/net/Ipep.h>
#include <frp/net/Socket.h>
#include <frp/collections/Dictionary.h>
using frp::configuration::MappingType;
using frp::net::AddressFamily;
using frp::net::Ipep;
using frp::net::IPEndPoint;
using frp::net::Socket;
using frp::collections::Dictionary;
namespace frp {
namespace discover {
Xndp::Xndp(const std::shared_ptr<frp::threading::Hosting>& hosting) noexcept
: Xndp(hosting, IPEndPoint::MinPort) {
}
Xndp::Xndp(const std::shared_ptr<frp::threading::Hosting>& hosting, int bindport) noexcept
: disposed_(false)
, aid_(0)
, bindport_(bindport)
, hosting_(hosting)
, context_(hosting->GetContext())
, buffer_(hosting->GetBuffer())
, socket_(*context_)
, timeout_(*context_) {
if (bindport < IPEndPoint::MinPort || bindport > IPEndPoint::MaxPort) {
bindport = IPEndPoint::MinPort;
}
if (!Socket::OpenSocket(socket_, boost::asio::ip::address_v6::any(), bindport)) {
bindport = IPEndPoint::MinPort;
}
else {
bindport = Socket::LocalPort(socket_);
}
boost::system::error_code ec;
if (bindport) {
boost::asio::ip::udp::endpoint localEP = socket_.local_endpoint(ec);
if (!ec) {
localEP_ = localEP;
}
}
}
boost::asio::ip::udp::endpoint Xndp::GetLocalEndPoint() noexcept {
return localEP_;
}
std::shared_ptr<boost::asio::io_context> Xndp::GetContext() noexcept {
return context_;
}
std::shared_ptr<frp::threading::Hosting> Xndp::GetHosting() noexcept {
return hosting_;
}
bool Xndp::IsAvailable() noexcept {
return !disposed_ && socket_.is_open();
}
void Xndp::Close() noexcept {
Dispose();
}
void Xndp::Dispose() noexcept {
if (!disposed_.exchange(true)) {
/* Releases the socket and timer held. */
Socket::Closesocket(socket_);
frp::threading::Hosting::Cancel(timeout_);
/* Releases all managed table member data. */
entries_.clear();
Dictionary::ReleaseAllPairs(callers_,
[this](XndpCallerContext& context) noexcept {
OnTimeoutError(context);
});
/* Deletes the message event listener bound to the current class instance object. */
MessageEvent = MessageEventHandler();
}
}
bool Xndp::Open() noexcept {
if (!IsAvailable()) {
return false;
}
return SetTimeout() && OnXndpProc();
}
bool Xndp::SetTimeout() noexcept {
if (disposed_) {
return false;
}
std::shared_ptr<Reference> reference = GetReference();
timeout_.expires_from_now(boost::posix_time::milliseconds(1000));
timeout_.async_wait(
[reference, this](const boost::system::error_code& ec) noexcept {
if (!ec) {
Timeout(hosting_->CurrentMillisec());
SetTimeout();
}
});
return true;
}
void Xndp::Timeout(UInt64 now) noexcept {
ProcessEntryTimeout(now);
ProcessCallerTimeout(now);
}
void Xndp::ProcessEntryTimeout(UInt64 now) noexcept {
XndpEntry1LevelTable::iterator tail = entries_.begin();
XndpEntry1LevelTable::iterator endl = entries_.end();
if (tail != endl) {
std::vector<XndpEntryPtr> releases;
for (; tail != endl; tail++) {
XndpEntry2LevelTable::iterator tail2 = tail->second.begin();
XndpEntry2LevelTable::iterator endl2 = tail->second.end();
for (; tail2 != endl2; tail2++) {
XndpEntryPtr& entry = tail2->second;
if (entry->last > now) {
releases.push_back(std::move(entry));
}
else {
UInt64 diff = now - entry->last;
if (diff >= XNDP_ENTRY_TIMEOUT) {
releases.push_back(std::move(entry));
}
}
}
}
for (std::size_t index = 0, length = releases.size(); index < length; index++) {
XndpEntryPtr& entry = releases[index];
if (entry) {
Dictionary::TryRemove2Layer(entries_, entry->type, entry->port);
}
}
}
}
void Xndp::ProcessCallerTimeout(UInt64 now) noexcept {
XndpCallerContextTable::iterator tail = callers_.begin();
XndpCallerContextTable::iterator endl = callers_.end();
if (tail != endl) {
std::vector<UInt32> releases;
for (; tail != endl; tail++) {
XndpCallerContext& context = tail->second;
if (!IsAvailable()) {
releases.push_back(context.id);
continue;
}
UInt64 initial_time = context.initial_time;
if (initial_time > now) {
releases.push_back(context.id);
continue;
}
UInt64 diff = now - initial_time;
if (diff >= XNDP_CALLER_TIMEOUT) {
releases.push_back(context.id);
continue;
}
UInt32 retry_count = ++context.retry_count;
if (retry_count >= 3) {
releases.push_back(context.id);
continue;
}
boost::system::error_code ec;
socket_.send_to(boost::asio::buffer(context.message.get(), context.message_size), context.destination, 0, ec);
}
for (std::size_t index = 0, length = releases.size(); index < length; index++) {
XndpCallerContext context;
if (Dictionary::TryRemove(callers_, releases[index], context)) {
OnTimeoutError(context);
}
}
}
}
void Xndp::OnTimeoutError(XndpCallerContext& context) noexcept {
if (context.register_ac) {
context.register_ac(false);
}
if (context.query_ac) {
boost::asio::ip::udp::endpoint nanoEP;
context.query_ac(false, nanoEP);
}
}
bool Xndp::OnXndpProc() noexcept {
if (!IsAvailable()) {
return false;
}
std::shared_ptr<Reference> reference = GetReference();
socket_.async_receive_from(boost::asio::buffer(buffer_.get(), frp::threading::Hosting::BufferSize), endpoint_,
[reference, this](const boost::system::error_code& ec, std::size_t sz) noexcept {
/* If it is an Xndp packet, the packet is processed; otherwise, the packet is displayed. */
int length = std::max<int>(ec ? -1 : sz, -1);
if (length >= sizeof(XndpPacket)) {
XndpPacket* packet = (XndpPacket*)buffer_.get();
do {
/* The key frame is inconsistent. */
if (packet->kf != XNDP_KF) {
break;
}
/* Check frame length validity. */
int len = ntohs(packet->len);
if (len != length) {
break;
}
/* Check packet checksum. */
int checksum = frp::messages::inet_chksum(packet, len);
if (checksum != 0) {
break;
}
/* This is a malicious external attack. */
if (packet->cmd != XNDP_REGST_ENTRY && packet->cmd != XNDP_QUERY_ENTRY && packet->cmd != XNDP_PUSH_MESSAGE) {
break;
}
/* Converts data from network byte order to local byte order. */
packet->len = len;
packet->id = ntohl(packet->id);
/* The action of processing the request. */
switch (packet->cmd) {
case XNDP_REGST_ENTRY:
if (bindport_) {
OnRegisterEntryServer(packet, endpoint_);
}
else {
OnRegisterEntryClient(packet);
}
break;
case XNDP_QUERY_ENTRY:
if (bindport_) {
OnQueryEntryServer(packet, endpoint_);
}
else {
OnQueryEntryClient(packet);
}
break;
case XNDP_PUSH_MESSAGE:
/* If the packets are not Xndp packets, they belong to other protocols that pass through the Intranet. */
int message_size = len - sizeof(XndpPacket);
if (message_size > 0) {
MessageEventHandler handler = MessageEvent;
if (handler) {
handler(packet->id, (Byte*)(packet + 1), message_size, endpoint_);
}
}
break;
};
} while (0);
}
OnXndpProc();
});
return true;
}
bool Xndp::OnRegisterEntryServer(XndpPacket* packet, const boost::asio::ip::udp::endpoint& remoteEP) noexcept {
XndpRegisterEntryRequestPacket* request = (XndpRegisterEntryRequestPacket*)packet;
if (request->len < sizeof(XndpRegisterEntryRequestPacket)) {
return false;
}
else {
request->port = ntohs(request->port);
}
XndpReisterReplyPacket reply;
reply.checksum = 0;
reply.id = packet->id;
reply.cmd = packet->cmd;
reply.len = sizeof(reply);
reply.err = XNDP_ERROR_SUCCESS;
if (request->type < MappingType::MappingType_TCP || request->type >= MappingType::MappingType_MaxType) {
reply.err = XNDP_ERROR_INVALID_ENTRY_TYPE;
}
elif(request->port <= IPEndPoint::MinPort || request->port > IPEndPoint::MaxPort) {
reply.err = XNDP_ERROR_INVALID_ENTRY_PORT;
}
else {
XndpEntryPtr& entry = entries_[request->type][request->port];
if (!entry) {
entry = make_shared_object<XndpEntry>();
}
entry->destinationEP = IPEndPoint::ToEndPoint<boost::asio::ip::udp>(IPEndPoint::V6ToV4(IPEndPoint::ToEndPoint(remoteEP)));
entry->port = request->port;
entry->type = request->type;
entry->last = hosting_->CurrentMillisec();
}
return ReplyToDestination(&reply, remoteEP);
}
bool Xndp::OnQueryEntryServer(XndpPacket* packet, const boost::asio::ip::udp::endpoint& remoteEP) noexcept {
XndpQueryEntryRequestPacket* request = (XndpQueryEntryRequestPacket*)packet;
if (request->len < sizeof(XndpQueryEntryRequestPacket)) {
return false;
}
else {
request->port = ntohs(request->port);
}
XndpQueryEntryReplyPacket reply;
reply.checksum = 0;
reply.id = packet->id;
reply.cmd = packet->cmd;
reply.len = sizeof(reply);
reply.err = XNDP_ERROR_SUCCESS;
reply.type = IPEndPoint::MinPort;
reply.port = MappingType::MappingType_TCP;
if (request->type < MappingType::MappingType_TCP || request->type >= MappingType::MappingType_MaxType) {
reply.err = XNDP_ERROR_INVALID_ENTRY_TYPE;
}
elif(request->port <= IPEndPoint::MinPort || request->port > IPEndPoint::MaxPort) {
reply.err = XNDP_ERROR_INVALID_ENTRY_PORT;
}
else {
XndpEntryPtr entry;
if (!Dictionary::TryGetValue2Layer(entries_, request->type, request->port, entry) || !entry) {
reply.err = XNDP_ERROR_NOT_FOUND_ENTRY;
}
else {
boost::asio::ip::address address = entry->destinationEP.address();
if (address.is_v4()) {
boost::asio::ip::address_v4::bytes_type address_bytes = address.to_v4().to_bytes();
memcpy(reply.dst_addr, address_bytes.data(), address_bytes.size());
reply.af = AddressFamily::InterNetwork;
}
else {
boost::asio::ip::address_v6::bytes_type address_bytes = address.to_v6().to_bytes();
memcpy(reply.dst_addr, address_bytes.data(), address_bytes.size());
reply.af = AddressFamily::InterNetworkV6;
}
reply.type = entry->type;
reply.port = htons(entry->port);
reply.dst_port = htons(entry->destinationEP.port());
}
}
return ReplyToDestination(&reply, remoteEP);
}
template<typename TCallback>
bool Xndp::RequireArgument(
frp::configuration::MappingType mapping_type,
int mapping_port,
TCallback& callback,
const boost::asio::ip::udp::endpoint& destinationEP) noexcept {
typedef frp::net::IPEndPoint IPEndPoint;
typedef frp::configuration::MappingType MappingType;
if (!IsAvailable()) {
return false;
}
elif(!callback) {
return false;
}
elif(mapping_type < MappingType::MappingType_TCP || mapping_type >= MappingType::MappingType_MaxType) {
return false;
}
elif(mapping_port <= IPEndPoint::MinPort || mapping_port > IPEndPoint::MaxPort) {
return false;
}
boost::asio::ip::address address = destinationEP.address();
if (address.is_multicast() || address.is_unspecified()) {
return false;
}
return true;
}
template<typename TXndpRequestPacket>
bool Xndp::AddXndpCaller(
Byte command,
Byte mapping_type,
int mapping_port,
const QueryAsyncCallback& qac,
const RegisterAsyncCallback& rac,
const boost::asio::ip::udp::endpoint& destinationEP) noexcept {
typedef frp::net::IPEndPoint IPEndPoint;
int message_size = sizeof(TXndpRequestPacket);
std::shared_ptr<Byte> message = make_shared_alloc<Byte>(message_size);
if (!message) {
return false;
}
UInt32 id = NewId();
TXndpRequestPacket* request = (TXndpRequestPacket*)message.get();
request->checksum = 0;
request->cmd = command;
request->id = id;
request->len = message_size;
request->type = mapping_type;
request->port = htons(mapping_port);
if (PacketSerializeMini(request) < 1) {
return false;
}
XNDP_ENDPOINT remoteEP = IPEndPoint::ToEndPoint<boost::asio::ip::udp>(IPEndPoint::V4ToV6(IPEndPoint::ToEndPoint(destinationEP)));
XndpCallerContext& context = callers_[id];
context.id = id;
context.destination = remoteEP;
context.initial_time = hosting_->CurrentMillisec();
context.query_ac = qac;
context.register_ac = rac;
context.retry_count = 0;
context.message = message;
context.message_size = message_size;
boost::system::error_code ec;
socket_.send_to(boost::asio::buffer(request, message_size), remoteEP, 0, ec);
return ec ? false : true;
}
int Xndp::PacketSerializeMini(XndpPacket* packet) noexcept {
int length = packet->len;
if (length < sizeof(XndpPacket)) {
return 0;
}
packet->kf = XNDP_KF;
packet->checksum = 0;
packet->id = htonl(packet->id);
packet->len = htons(length);
UInt16 checksum = frp::messages::inet_chksum(packet, length);
if (checksum == 0) {
checksum = 0xffff;
}
packet->checksum = htons(checksum);
return length;
}
bool Xndp::ReplyToDestination(XndpPacket* packet, const boost::asio::ip::udp::endpoint& remoteEP) noexcept {
if (!IsAvailable()) {
return false;
}
int length = PacketSerializeMini(packet);
if (length < 1) {
return false;
}
boost::system::error_code ec;
socket_.send_to(boost::asio::buffer(packet, length), remoteEP, 0, ec);
return ec ? false : true;
}
bool Xndp::RegisterAsync(MappingType mapping_type, int mapping_port, RegisterAsyncCallback callback, const boost::asio::ip::udp::endpoint& destinationEP) noexcept {
if (!RequireArgument(mapping_type, mapping_port, callback, destinationEP)) {
return false;
}
return AddXndpCaller<XndpRegisterEntryRequestPacket>(XNDP_REGST_ENTRY,
mapping_type, mapping_port, NULL, callback, destinationEP);
}
bool Xndp::QueryAsync(MappingType mapping_type, int mapping_port, QueryAsyncCallback callback, const boost::asio::ip::udp::endpoint& destinationEP) noexcept {
if (!RequireArgument(mapping_type, mapping_port, callback, destinationEP)) {
return false;
}
return AddXndpCaller<XndpQueryEntryRequestPacket>(XNDP_QUERY_ENTRY,
mapping_type, mapping_port, callback, NULL, destinationEP);
}
bool Xndp::PushAsync(UInt32 id, const void* message, const int message_size, const boost::asio::ip::udp::endpoint& destinationEP) noexcept {
if (!message || message_size < 1) {
return false;
}
int chunk_size = sizeof(XndpPacket) + message_size;
std::shared_ptr<Byte> chunk = make_shared_alloc<Byte>(chunk_size);
if (!message) {
return false;
}
XndpPacket* packet = (XndpPacket*)chunk.get();
packet->checksum = 0;
packet->cmd = XNDP_PUSH_MESSAGE;
packet->id = id;
packet->len = chunk_size;
memcpy(packet + 1, message, message_size);
if (PacketSerializeMini(packet) < 1) {
return false;
}
boost::system::error_code ec;
socket_.send_to(boost::asio::buffer(packet, chunk_size), destinationEP, 0, ec);
return ec ? false : true;
}
UInt32 Xndp::NewId() noexcept {
for (;;) {
int id = aid_++;
if (!id) {
continue;
}
if (!Dictionary::ContainsKey(callers_, id)) {
return id;
}
}
}
boost::asio::ip::udp::socket& Xndp::GetSocket() noexcept {
return socket_;
}
bool Xndp::PopCallerContext(UInt32 id, XndpCallerContext& context) noexcept {
return Dictionary::TryRemove(callers_, id, context);
}
bool Xndp::OnRegisterEntryClient(XndpPacket* packet) noexcept {
XndpReisterReplyPacket* reply = (XndpReisterReplyPacket*)packet;
if (reply->len < sizeof(XndpReisterReplyPacket)) {
return false;
}
XndpCallerContext caller;
if (!PopCallerContext(reply->id, caller)) {
return false;
}
caller.register_ac(reply->err == XNDP_ERROR_SUCCESS);
return true;
}
bool Xndp::OnQueryEntryClient(XndpPacket* packet) noexcept {
XndpQueryEntryReplyPacket* reply = (XndpQueryEntryReplyPacket*)packet;
if (reply->len < sizeof(XndpQueryEntryReplyPacket)) {
return false;
}
XndpCallerContext caller;
if (!PopCallerContext(reply->id, caller)) {
return false;
}
if (reply->err != XNDP_ERROR_SUCCESS) {
boost::asio::ip::udp::endpoint noneEP;
caller.query_ac(false, noneEP);
}
else {
boost::asio::ip::address address;
if (reply->af == AddressFamily::InterNetwork) {
address = boost::asio::ip::address_v4(*(boost::asio::ip::address_v4::bytes_type*)reply->dst_addr);
}
else {
address = boost::asio::ip::address_v6(*(boost::asio::ip::address_v6::bytes_type*)reply->dst_addr);
}
boost::asio::ip::udp::endpoint destinationEP(address, ntohs(reply->dst_port));
caller.query_ac(true, destinationEP);
}
return true;
}
boost::asio::ip::udp::endpoint Xndp::ToEndPoint(const std::string host, int port) noexcept {
return IPEndPoint::ToEndPoint<boost::asio::ip::udp>(Ipep::GetEndPoint(host, port));
}
}
}