Skip to content

Instantly share code, notes, and snippets.

@jyaif
Last active April 9, 2024 09:07
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jyaif/e0db3a680443730c05ca36be26f22c93 to your computer and use it in GitHub Desktop.
Save jyaif/e0db3a680443730c05ca36be26f22c93 to your computer and use it in GitHub Desktop.
A simple stun client in C++ (posix only)
// How to compile:
// clang++ -std=c++11 stun.cpp
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <array>
#include <cstring>
#include <string>
constexpr uint16_t kBindingRequest = 0x0001;
constexpr uint16_t kBindingResponse = 0x0101;
constexpr uint16_t kXorMappedAddress = 0x0020;
constexpr uint32_t kMagicCookie = 0x2112A442;
// https://datatracker.ietf.org/doc/html/rfc5389#section-6
struct __attribute__((packed)) StunRequest {
StunRequest() {
for (int i = 0; i < transaction_id_.size(); i++) {
transaction_id_[i] = rand() % 256;
}
}
const int16_t stun_message_type_ = htons(kBindingRequest);
const int16_t message_length_ = htons(0x0000);
const int32_t magic_cookie_ = htonl(kMagicCookie);
std::array<uint8_t, 12> transaction_id_;
};
// https://datatracker.ietf.org/doc/html/rfc5389#section-7
struct __attribute__((packed)) StunResponse {
int16_t stun_message_type_;
int16_t message_length_;
int32_t magic_cookie_;
std::array<uint8_t, 12> transaction_id_;
std::array<uint8_t, 1000> attributes_;
};
struct IPAndPort {
std::string ip_;
uint16_t port_ = 0;
};
// Converts a host name into an IP address.
std::string HostnameToIP(std::string const& hostname) {
char ip[100];
struct addrinfo hints, *servinfo, *p;
struct sockaddr_in* h;
int rv;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if ((rv = getaddrinfo(hostname.c_str(), "http", &hints, &servinfo)) != 0) {
return "";
}
for (p = servinfo; p != NULL; p = p->ai_next) {
h = (struct sockaddr_in*)p->ai_addr;
strcpy(ip, inet_ntoa(h->sin_addr));
}
freeaddrinfo(servinfo);
return std::string(ip);
}
// Returns true on success
bool PerformStunRequest(int socket_fd,
std::string const& stun_server_ip,
short stun_server_port,
short local_port,
IPAndPort& ip_and_port) {
struct sockaddr_in servaddr;
struct sockaddr_in localaddr;
StunRequest stun_request;
StunResponse stun_response;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, stun_server_ip.c_str(), &servaddr.sin_addr);
servaddr.sin_port = htons(stun_server_port);
bzero(&localaddr, sizeof(localaddr));
localaddr.sin_family = AF_INET;
localaddr.sin_port = htons(local_port);
int err = bind(socket_fd, (struct sockaddr*)&localaddr, sizeof(localaddr));
if (err < 0) {
printf("bind error\n");
return false;
}
printf("Sending STUN request to %s:%d\n", stun_server_ip.c_str(),
stun_server_port);
err = sendto(socket_fd, &stun_request, sizeof(stun_request), 0,
(struct sockaddr*)&servaddr, sizeof(servaddr));
if (err < 0) {
printf("sendto error\n");
return false;
}
err = recvfrom(socket_fd, &stun_response, sizeof(stun_response), 0, NULL, 0);
if (err < 0) {
printf("recvfrom error\n");
return false;
}
if (stun_response.magic_cookie_ != htonl(kMagicCookie)) {
printf("magic cookie of response does not match request\n");
return false;
}
if (stun_response.transaction_id_ != stun_request.transaction_id_) {
printf("incorrect transaction id\n");
return false;
}
if (stun_response.stun_message_type_ != htons(kBindingResponse)) {
printf("incorrect message type\n");
return false;
}
auto const& attributes = stun_response.attributes_;
int16_t attributes_length =
std::min<int16_t>(htons(stun_response.message_length_),
stun_response.attributes_.size());
int i = 0;
while (i < attributes_length) {
auto attribute_type = htons(*(int16_t*)(&attributes[i]));
auto attribute_length = htons(*(int16_t*)(&attributes[i + 2]));
if (attribute_type == kXorMappedAddress) {
uint16_t port = ntohs(*(uint16_t*)(&attributes[i + 6]));
port ^= (kMagicCookie >> 16);
std::string ip = std::to_string(attributes[i + 8] ^ ((kMagicCookie & 0xff000000) >> 24)) + "." +
std::to_string(attributes[i + 9] ^ ((kMagicCookie & 0x00ff0000) >> 16)) + "." +
std::to_string(attributes[i + 10] ^ ((kMagicCookie & 0x0000ff00) >> 8)) + "." +
std::to_string(attributes[i + 11] ^ ((kMagicCookie & 0x000000ff) >> 0)) + ".";
ip_and_port.ip_ = ip;
ip_and_port.port_ = port;
return true;
}
i += (4 + attribute_length);
}
return false;
}
int main(int argc, char* argv[]) {
const char* stun_server_host_name = "stun2.l.google.com";
std::string ip = HostnameToIP(stun_server_host_name);
printf("%s resolved to %s\n", stun_server_host_name, ip.c_str());
IPAndPort ip_and_port;
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
bool stun_request_succesfull =
PerformStunRequest(socket_fd, ip, 19305, 9910, ip_and_port);
close(socket_fd);
if (stun_request_succesfull) {
printf("local ip:port = %s:%d\n", ip_and_port.ip_.c_str(),
ip_and_port.port_);
} else {
printf("Failed to perform stun request");
}
}
@oliverepper
Copy link

This is nice but it has bugs. The conversion of the port number will not work for a port number that will not fit into a signed 16 bit integer. And it would be nice to use the actual message_cookie for the xor calculations. Anyways converting a port number greater than 32767 will give you a negative port number with this algorithm.

@jyaif
Copy link
Author

jyaif commented Jan 17, 2024

Fixed the bug and addressed your clean up suggestion. Thank you!

@lvckydrip
Copy link

lvckydrip commented Jan 18, 2024

I have a question, where do you get the attribute types from?
I read the site about it, but I can't find where the signature of the types are.
For example, the constexpr uint16_t kXorMappedAddress = 0x0020;
But very nice work!

@oliverepper
Copy link

See section 18.2:

Comprehension-required range (0x0000-0x7FFF):
0x0000: (Reserved)
0x0001: MAPPED-ADDRESS
0x0002: (Reserved; was RESPONSE-ADDRESS)
0x0003: (Reserved; was CHANGE-ADDRESS)
0x0004: (Reserved; was SOURCE-ADDRESS)
0x0005: (Reserved; was CHANGED-ADDRESS)
0x0006: USERNAME
0x0007: (Reserved; was PASSWORD)
0x0008: MESSAGE-INTEGRITY
0x0009: ERROR-CODE
0x000A: UNKNOWN-ATTRIBUTES
0x000B: (Reserved; was REFLECTED-FROM)
0x0014: REALM
0x0015: NONCE
0x0020: XOR-MAPPED-ADDRESS

@lvckydrip
Copy link

See section 18.2:

Comprehension-required range (0x0000-0x7FFF): 0x0000: (Reserved) 0x0001: MAPPED-ADDRESS 0x0002: (Reserved; was RESPONSE-ADDRESS) 0x0003: (Reserved; was CHANGE-ADDRESS) 0x0004: (Reserved; was SOURCE-ADDRESS) 0x0005: (Reserved; was CHANGED-ADDRESS) 0x0006: USERNAME 0x0007: (Reserved; was PASSWORD) 0x0008: MESSAGE-INTEGRITY 0x0009: ERROR-CODE 0x000A: UNKNOWN-ATTRIBUTES 0x000B: (Reserved; was REFLECTED-FROM) 0x0014: REALM 0x0015: NONCE 0x0020: XOR-MAPPED-ADDRESS

Oh i didnt saw that thank you for your fast reply. I thought it was under STUN Attributes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment