├── .gitignore ├── Makefile ├── README.md ├── README_en.md ├── p2pchat ├── Makefile ├── README.md ├── client.c ├── endpoint.c ├── endpoint.h ├── endpoint_list.c ├── endpoint_list.h ├── logging.c ├── logging.h ├── message.c ├── message.h ├── server.c └── tests │ ├── Makefile │ ├── test_endpoint.c │ ├── test_eplist.c │ ├── test_logging.c │ └── test_message.c ├── run_test.sh ├── stun ├── README.md └── classic_stun_client.py └── tools ├── Makefile ├── client.py ├── udp_client.c └── udp_server.c /.gitignore: -------------------------------------------------------------------------------- 1 | server 2 | client 3 | *.swo 4 | *.swp 5 | tests/test_list 6 | tools/udp_server 7 | tools/udp_client 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS := -g -std=c99 -Wall -Wno-implicit-function-declaration 2 | TARGETS := p2pchat tools 3 | all: $(TARGETS) 4 | 5 | p2pchat: 6 | $(MAKE) -C p2pchat 7 | test: 8 | $(MAKE) -C p2pchat/tests 9 | tool: 10 | $(MAKE) -C tools 11 | 12 | clean: 13 | make -C p2pchat clean 14 | make -C p2pchat/tests clean 15 | make -C tools clean 16 | .PHONY: clean p2pchat test tools 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # P2P-Over-MiddleBoxes-Demo 2 | 3 | 一个简单的P2P通信示例 4 | 5 | [English README](README_en.md) 6 | 7 | # p2pchat 8 | 9 | 一个P2P聊天程序,使用UDP打洞创建链接。 10 | 11 | ## 编译 12 | 13 | make p2pchat 14 | 15 | ## 运行 16 | 17 | ./p2pchat/server <服务器端口号> 18 | 19 | ./p2pchat/client <服务器IP>:<服务器端口号> 20 | >>> help 21 | 22 | # 测试: 23 | 24 | ## 编译 25 | 26 | make test 27 | 28 | ## 运行 29 | 30 | ./run_test.sh 31 | 32 | # 常见问题 33 | 34 | ## 通信不正常/打洞不成功? 35 | 该UDP打洞示例仅支持锥形地址转换器(Cone NAT),如果两个客户端都在同一个公网结点下,需要确保出口路由器支持**回环传输(LOOPBACK TRANSMISSION)**。 36 | 37 | ## 我咋知道我的NAT是什么类型? 38 | 在[stun目录下](stun)有个简单的Python脚本,用RFC3489(经典STUN协议)的示例来检测NAT类型。 39 | 运行: 40 | ``` 41 | cd stun 42 | python3 classic_stun_client.py [本地IP] 43 | ``` 44 | 45 | 运行结果示例如下: 46 | ``` 47 | INFO:root:running test I with stun.ideasip.com:3478 48 | INFO:root:MAPPED_ADDRESS: 220.181.57.217:46208 49 | INFO:root:running test II with stun.ideasip.com:3478 50 | INFO:root:running test I with 217.116.122.138:3479 51 | INFO:root:MAPPED_ADDRESS: 220.181.57.217:2732 52 | NAT_TYPE: Symmetric NAT 53 | ``` 54 | 55 | 56 | # 相关介绍文章 57 | 58 | - [https://evilpan.com/2015/10/31/p2p-over-middle-box/][blog] 59 | 60 | > 注: 本项目只是一个简单的UDP打洞示例,如果想构建成熟的P2P应用,可以接着参考STUN/TURN以及ICE等协议。 61 | 62 | [jekyll]:http://jekyll.pppan.net/2015/10/31/p2p-over-middle-box/ 63 | [django]:https://www.pppan.net/blog/detail/2017-12-16-p2p-over-middle-box 64 | [blog]: https://evilpan.com/2015/10/31/p2p-over-middle-box/ 65 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # P2P-Over-MiddleBoxes-Demo 2 | A simple demo of P2P communication over middle boxes such as NAT 3 | 4 | # p2pchat 5 | 6 | ## build: 7 | 8 | make p2pchat 9 | 10 | ## run: 11 | 12 | ./p2pchat/server port 13 | 14 | ./p2pchat/client server:port 15 | >>> help 16 | 17 | # test 18 | 19 | ## build: 20 | 21 | make test 22 | 23 | ## run: 24 | 25 | ./run_test.sh 26 | 27 | # FAQ 28 | 29 | ## It doesn't work? 30 | This UDP hole punching demo only works on Cone NAT. 31 | 32 | ## How to check my NAT type? 33 | There is a simple python script to test your NAT type using RFC3489(the classic STUN protocol) in [stun](stun). 34 | You can simply check it by running: 35 | ``` 36 | cd stun 37 | python3 classic_stun_client.py [your-local-ip] 38 | ``` 39 | 40 | And the result would be similar with: 41 | ``` 42 | INFO:root:running test I with stun.ideasip.com:3478 43 | INFO:root:MAPPED_ADDRESS: 220.181.57.217:46208 44 | INFO:root:running test II with stun.ideasip.com:3478 45 | INFO:root:running test I with 217.116.122.138:3479 46 | INFO:root:MAPPED_ADDRESS: 220.181.57.217:2732 47 | NAT_TYPE: Symmetric NAT 48 | ``` 49 | 50 | ## My NAT is cone NAT, but it still doesn't work 51 | If two of your peers are both behind the same NAT, this NAT must support `LOOPBACK TRANSMISSION` 52 | to forward messages. You can test it by using the utils(`udp_server/udp_client`) in [tools](tools) 53 | 54 | # related post (in Chinese) 55 | 56 | - [https://evilpan.com/2015/10/31/p2p-over-middle-box/][blog] 57 | 58 | > NOTE: This is just a proof of concept project. If you want to build a stable 59 | > P2P application, please refer to STUN/TURN and ICE protocol as well. 60 | 61 | [jekyll]:http://jekyll.pppan.net/2015/10/31/p2p-over-middle-box/ 62 | [django]:https://www.pppan.net/blog/detail/2017-12-16-p2p-over-middle-box 63 | [blog]: https://evilpan.com/2015/10/31/p2p-over-middle-box/ 64 | -------------------------------------------------------------------------------- /p2pchat/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS := -g -std=c99 -Wall -Wno-implicit-function-declaration 2 | TARGETS := server client 3 | all: $(TARGETS) 4 | 5 | client : client.c endpoint.c message.c logging.c endpoint_list.c 6 | gcc $^ -o $@ $(CFLAGS) -pthread 7 | server : server.c endpoint.c message.c logging.c endpoint_list.c 8 | gcc $^ -o $@ $(CFLAGS) 9 | 10 | test: 11 | $(MAKE) -C tests 12 | 13 | clean: 14 | rm -f $(TARGETS) *.o 15 | .PHONY: clean 16 | -------------------------------------------------------------------------------- /p2pchat/README.md: -------------------------------------------------------------------------------- 1 | This p2pchat demo is based on UDP protocol, and works only when 2 | 3 | 1) both of peers are behind `Cone NAT`. 4 | 2) if both peers are behind the same NAT, then this NAT must support `loopback transmission` to forward message. 5 | 6 | It's a proof-of-concept project, and not importing some standard protocol like STUN or TURN yet. 7 | -------------------------------------------------------------------------------- /p2pchat/client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "logging.h" 8 | #include "message.h" 9 | #include "endpoint.h" 10 | #include "endpoint_list.h" 11 | #define PING_INTERVAL 10 12 | 13 | static int quiting = 0; 14 | static int g_clientfd; 15 | static endpoint_t g_server; 16 | static eplist_t *g_peers; 17 | void *keepalive_loop(); 18 | void *receive_loop(); 19 | void *console_loop(); 20 | 21 | static void quit() { 22 | quiting = 1; 23 | } 24 | 25 | void *keepalive_loop() { 26 | Message ping; 27 | ping.head.magic = MSG_MAGIC; 28 | ping.head.type = MTYPE_PING; 29 | ping.head.length = 0; 30 | ping.body = NULL; 31 | unsigned int i = 0; 32 | while(!quiting) { 33 | // quit ASAP 34 | if (i++ < PING_INTERVAL) { 35 | sleep(1); 36 | continue; 37 | } 38 | i = 0; 39 | udp_send_msg(g_clientfd, g_server, ping); 40 | for (eplist_t *ep = g_peers->next; ep != NULL; ep = ep->next) { 41 | udp_send_msg(g_clientfd, ep->endpoint, ping); 42 | } 43 | } 44 | log_info("quiting keepalive_loop"); 45 | return NULL; 46 | } 47 | 48 | void on_message(endpoint_t from, Message msg) { 49 | log_debug("RECV %d bytes FROM %s: %s %s", msg.head.length, 50 | ep_tostring(from), strmtype(msg.head.type), msg.body); 51 | // from server 52 | if (ep_equal(g_server, from)) { 53 | switch (msg.head.type) { 54 | case MTYPE_PUNCH: 55 | { 56 | endpoint_t peer = ep_fromstring(msg.body); 57 | log_info("%s on call, replying...", ep_tostring(peer)); 58 | udp_send_text(g_clientfd, peer, MTYPE_REPLY, NULL); 59 | } 60 | break; 61 | case MTYPE_REPLY: 62 | log_info("SERVER: %s", msg.body); 63 | break; 64 | default: 65 | break; 66 | } 67 | return; 68 | } 69 | // from peer 70 | switch (msg.head.type) { 71 | case MTYPE_TEXT: 72 | log_info("Peer(%s): %s", ep_tostring(from), msg.body); 73 | break; 74 | case MTYPE_REPLY: 75 | log_info("Peer(%s) replied, you can talk now", ep_tostring(from)); 76 | eplist_add(g_peers, from); 77 | case MTYPE_PUNCH: 78 | /* 79 | * Usually we can't recevie punch request from other peer directly, 80 | * but it could happen when it come after we reply the punch request from server, 81 | * or there's a tunnel already. 82 | * */ 83 | udp_send_text(g_clientfd, from, MTYPE_TEXT, "I SEE YOU"); 84 | break; 85 | case MTYPE_PING: 86 | udp_send_text(g_clientfd, from, MTYPE_PONG, NULL); 87 | default: 88 | break; 89 | } 90 | } 91 | void *receive_loop() { 92 | endpoint_t peer; 93 | socklen_t addrlen; 94 | char buf[RECV_BUFSIZE]; 95 | int nfds; 96 | fd_set readfds; 97 | struct timeval timeout; 98 | 99 | nfds = g_clientfd + 1; 100 | while(!quiting) { 101 | FD_ZERO(&readfds); 102 | FD_SET(g_clientfd, &readfds); 103 | timeout.tv_sec = 1; 104 | timeout.tv_usec = 0; 105 | int ret = select(nfds, &readfds, NULL, NULL, &timeout); 106 | if (ret == 0) { 107 | /* timeout */ 108 | continue; 109 | } 110 | else if (ret == -1) { 111 | perror("select"); 112 | continue; 113 | } 114 | assert(FD_ISSET(g_clientfd, &readfds)); 115 | addrlen = sizeof(peer); 116 | memset(&peer, 0, addrlen); 117 | memset(buf, 0, RECV_BUFSIZE); 118 | int rd_size = recvfrom(g_clientfd, buf, RECV_BUFSIZE, 0, 119 | (struct sockaddr*)&peer, &addrlen); 120 | if (rd_size == -1) { 121 | perror("recvfrom"); 122 | continue; 123 | } else if (rd_size == 0) { 124 | log_info("EOF from %s", ep_tostring(peer)); 125 | continue; 126 | } 127 | Message msg = msg_unpack(buf, rd_size); 128 | if (msg.head.magic != MSG_MAGIC || msg.body == NULL) { 129 | log_warn("Invalid message(%d bytes): {0x%x,%d,%d} %p", rd_size, 130 | msg.head.magic, msg.head.type, msg.head.length, msg.body); 131 | continue; 132 | } 133 | on_message(peer, msg); 134 | } 135 | log_info("quiting receive_loop"); 136 | return NULL; 137 | } 138 | 139 | static void print_help() 140 | { 141 | const static char *help_message = "" 142 | "Usage:" 143 | "\n\n login" 144 | "\n login to server so that other peer(s) can see you" 145 | "\n\n logout" 146 | "\n logout from server" 147 | "\n\n list" 148 | "\n list logined peers" 149 | "\n\n punch host:port" 150 | "\n punch a hole through UDP to [host:port]" 151 | "\n host:port must have been logged in to server" 152 | "\n Example:" 153 | "\n >>> punch 9.8.8.8:53" 154 | "\n\n send host:port data" 155 | "\n send [data] to peer [host:port] through UDP protocol" 156 | "\n the other peer could receive your message if UDP hole punching succeed" 157 | "\n Example:" 158 | "\n >>> send 8.8.8.8:53 hello" 159 | "\n\n help" 160 | "\n print this help message" 161 | "\n\n quit" 162 | "\n logout and quit this program"; 163 | printf("%s\n", help_message); 164 | } 165 | void *console_loop() { 166 | char *line = NULL; 167 | size_t len; 168 | ssize_t read; 169 | while(fprintf(stdout, ">>> ") && (read = getline(&line, &len, stdin)) != -1) { 170 | /* ignore empty line */ 171 | if (read == 1) continue; 172 | char *cmd = strtok(line, " "); 173 | if (strncmp(cmd, "list", 4) == 0) { 174 | udp_send_text(g_clientfd, g_server, MTYPE_LIST, NULL); 175 | } else if (strncmp(cmd, "login", 5) == 0) { 176 | udp_send_text(g_clientfd, g_server, MTYPE_LOGIN, NULL); 177 | } else if (strncmp(cmd, "logout", 5) == 0) { 178 | udp_send_text(g_clientfd, g_server, MTYPE_LOGOUT, NULL); 179 | } else if (strncmp(cmd, "punch", 5) == 0) { 180 | char *host_port = strtok(NULL, "\n"); 181 | endpoint_t peer = ep_fromstring(host_port); 182 | log_info("punching %s", ep_tostring(peer)); 183 | udp_send_text(g_clientfd, peer, MTYPE_PUNCH, NULL); 184 | udp_send_text(g_clientfd, g_server, MTYPE_PUNCH, host_port); 185 | } else if (strncmp(cmd, "send", 4) == 0) { 186 | char *host_port = strtok(NULL, " "); 187 | char *data = strtok(NULL, "\n"); 188 | udp_send_text(g_clientfd, ep_fromstring(host_port), MTYPE_TEXT, data); 189 | } else if (strncmp(cmd, "help", 4) == 0) { 190 | print_help(); 191 | } else if (strncmp(cmd, "quit", 4) == 0) { 192 | udp_send_text(g_clientfd, g_server, MTYPE_LOGOUT, NULL); 193 | quit(); 194 | break; 195 | } else { 196 | printf("Unknown command %s\n", cmd); 197 | print_help(); 198 | } 199 | } 200 | free(line); 201 | log_info("quiting console_loop"); 202 | return NULL; 203 | } 204 | 205 | int main(int argc, char **argv) 206 | { 207 | log_setlevel(INFO); 208 | if (argc != 2) { 209 | fprintf(stderr, "Usage: %s server:port\n", argv[0]); 210 | return 1; 211 | } 212 | int ret; 213 | pthread_t keepalive_pid, receive_pid, console_pid; 214 | 215 | g_server = ep_fromstring(argv[1]); 216 | g_peers = eplist_create(); 217 | g_clientfd = socket(AF_INET, SOCK_DGRAM, 0); 218 | log_info("setting server to %s", ep_tostring(g_server)); 219 | if (g_clientfd == -1) { perror("socket"); goto clean; } 220 | ret = pthread_create(&keepalive_pid, NULL, &keepalive_loop, NULL); 221 | if (ret != 0) { perror("keepalive"); goto clean; } 222 | ret = pthread_create(&receive_pid, NULL, &receive_loop, NULL); 223 | if (ret != 0) { perror("receive"); goto clean; } 224 | ret = pthread_create(&console_pid, NULL, &console_loop, NULL); 225 | if (ret != 0) { perror("console"); goto clean; } 226 | 227 | pthread_join(console_pid, NULL); 228 | pthread_join(receive_pid, NULL); 229 | pthread_join(keepalive_pid, NULL); 230 | clean: 231 | close(g_clientfd); 232 | eplist_destroy(g_peers); 233 | return 0; 234 | } 235 | -------------------------------------------------------------------------------- /p2pchat/endpoint.c: -------------------------------------------------------------------------------- 1 | #include "endpoint.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define INET_PORTSTRLEN 5 8 | #define TUPLE_LEN (INET_ADDRSTRLEN + INET_PORTSTRLEN + 1) 9 | int ep_equal(endpoint_t lp, endpoint_t rp) { 10 | return ( (lp.sin_family == rp.sin_family) && 11 | (lp.sin_addr.s_addr == rp.sin_addr.s_addr) && 12 | (lp.sin_port == rp.sin_port) ); 13 | } 14 | 15 | /* NOT THREAD SAFE */ 16 | char *ep_tostring(endpoint_t ep) { 17 | static char tuple[TUPLE_LEN]; 18 | snprintf(tuple, TUPLE_LEN, "%s:%d", 19 | inet_ntoa(ep.sin_addr), 20 | ntohs(ep.sin_port)); 21 | return tuple; 22 | } 23 | 24 | endpoint_t ep_fromstring(const char *tuple) { 25 | char _tuple[TUPLE_LEN]; 26 | char *host = NULL; 27 | char *port = NULL; 28 | sprintf(_tuple, "%s", tuple); 29 | host = strtok(_tuple, ":"); 30 | port = strtok(NULL, ":"); 31 | if (host == NULL || port == NULL) { 32 | host = "255.255.255.255"; 33 | port = "0"; 34 | } 35 | return ep_frompair(host, atoi(port)); 36 | } 37 | 38 | endpoint_t ep_frompair(const char *host, short port) { 39 | endpoint_t ep; 40 | memset(&ep, 0, sizeof ep); 41 | ep.sin_family = AF_INET; 42 | ep.sin_addr.s_addr = inet_addr(host); 43 | ep.sin_port = htons(port); 44 | return ep; 45 | } 46 | -------------------------------------------------------------------------------- /p2pchat/endpoint.h: -------------------------------------------------------------------------------- 1 | #ifndef P2PCHAT_ENDPOINT_H 2 | #define P2PCHAT_ENDPOINT_H 3 | #include 4 | typedef struct sockaddr_in endpoint_t; 5 | //struct _EndPoint { 6 | // // network byte order(big endian) 7 | // int ip; 8 | // short port; 9 | //}/*__attribute__((packed))*/; 10 | 11 | int ep_equal(endpoint_t lp, endpoint_t rp); 12 | 13 | /* string is host:port format */ 14 | /* IPV4 ONLY */ 15 | char *ep_tostring(endpoint_t ep); 16 | endpoint_t ep_fromstring(const char *tuple); 17 | endpoint_t ep_frompair(const char *ip, short port); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /p2pchat/endpoint_list.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "endpoint_list.h" 7 | 8 | // head is dummpy 9 | eplist_t *eplist_create() { 10 | eplist_t *head = (eplist_t *)malloc(sizeof(eplist_t)); 11 | head->lastseen = -1; 12 | head->next = NULL; 13 | return head; 14 | } 15 | 16 | void eplist_destroy(eplist_t *head) { 17 | if (head == NULL) return; 18 | eplist_destroy(head->next); 19 | head->next = NULL; 20 | free(head); 21 | } 22 | 23 | int eplist_add(eplist_t *head, endpoint_t ep) { 24 | eplist_t *current; 25 | for (current = head; current != NULL; current = current->next) { 26 | if (current != head && ep_equal(ep, current->endpoint)) { 27 | // do not add existing endpoint 28 | return 1; 29 | } else if (current->next == NULL) { 30 | eplist_t *newep = (eplist_t *)malloc(sizeof(eplist_t)); 31 | newep->lastseen = time(NULL); 32 | newep->endpoint = ep; 33 | newep->next = NULL; 34 | current->next = newep; 35 | return 0; 36 | } 37 | } 38 | /* shouldn't be here */ 39 | assert(1); 40 | return 1; 41 | } 42 | 43 | int eplist_remove(eplist_t *head, endpoint_t ep) { 44 | eplist_t *current; 45 | for (current = head; 46 | current != NULL && current->next != NULL; 47 | current = current->next) { 48 | if (ep_equal(ep, current->next->endpoint)) { 49 | eplist_t *temp = current->next; 50 | current->next = temp->next; 51 | free(temp); 52 | return 0; 53 | } 54 | } 55 | return 1; 56 | } 57 | 58 | int eplist_count(eplist_t *head) { 59 | int i = -1; 60 | for (eplist_t *current = head; current != NULL; current = current->next) { 61 | i++; 62 | } 63 | return i; 64 | } 65 | 66 | void eplist_dump(eplist_t *head) { 67 | if (head == NULL) return; 68 | 69 | eplist_t *current; 70 | for (current = head->next; current != NULL; current = current->next) { 71 | printf("%s(%ld)%s", ep_tostring(current->endpoint), 72 | current->lastseen, current->next ? "->" : ";\n"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /p2pchat/endpoint_list.h: -------------------------------------------------------------------------------- 1 | #ifndef P2P_ENDPOINT_LIST_H 2 | #define P2P_ENDPOINT_LIST_H 3 | 4 | #include "endpoint.h" 5 | typedef struct _eplist_t eplist_t; 6 | struct _eplist_t { 7 | endpoint_t endpoint; 8 | time_t lastseen; 9 | eplist_t *next; 10 | }; 11 | 12 | /* 13 | * create a new eplist_t *. Don't forget to destroy it with eplist_destroy() 14 | * */ 15 | eplist_t *eplist_create(); 16 | void eplist_destroy(eplist_t *head); 17 | int eplist_add(eplist_t *head, endpoint_t ep); 18 | int eplist_remove(eplist_t *head, endpoint_t ep); 19 | int eplist_count(eplist_t *head); 20 | void eplist_dump(eplist_t *head); 21 | #endif 22 | -------------------------------------------------------------------------------- /p2pchat/logging.c: -------------------------------------------------------------------------------- 1 | #include "logging.h" 2 | 3 | static LogLevel CURRENT_LEVEL = INFO; 4 | void log_setlevel(LogLevel level) { 5 | CURRENT_LEVEL = level; 6 | } 7 | LogLevel log_getlevel() { 8 | return CURRENT_LEVEL; 9 | } 10 | const char *levelstr(LogLevel level) { 11 | switch (level) { 12 | case DEBUG: 13 | return "DBG"; 14 | case INFO: 15 | return "INF"; 16 | case WARN: 17 | return "WAR"; 18 | case ERROR: 19 | return "ERR"; 20 | default: 21 | return "???"; 22 | } 23 | } 24 | void log_msg(LogLevel level, const char *msg, ...) { 25 | if (level < CURRENT_LEVEL) return; 26 | va_list ap; 27 | va_start(ap, msg); 28 | char fmt[LOGBUF] = {0}; 29 | char timestamp[26]; 30 | struct timeb tp; 31 | ftime(&tp); 32 | struct tm *st = localtime(&tp.time); 33 | sprintf(timestamp, "%02d:%02d:%02d.%03d", 34 | st->tm_hour, st->tm_min, st->tm_sec, tp.millitm); 35 | sprintf(fmt, "%s %s: ", timestamp, levelstr(level)); 36 | strcat(fmt, msg); 37 | strcat(fmt, "\n"); 38 | vprintf(fmt, ap); 39 | va_end(ap); 40 | } 41 | -------------------------------------------------------------------------------- /p2pchat/logging.h: -------------------------------------------------------------------------------- 1 | #ifndef P2P_CHAT_LOGGING_H 2 | #define P2P_CHAT_LOGGING_H 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define LOGBUF 1024 10 | 11 | #define log_debug(msg, args...) log_msg(DEBUG, msg, ##args) 12 | #define log_info(msg, args...) log_msg(INFO, msg, ##args) 13 | #define log_warn(msg, args...) log_msg(WARN, msg, ##args) 14 | #define log_err(msg, args...) log_msg(ERROR, msg, ##args) 15 | typedef enum _LogLevel { 16 | DEBUG = 0, 17 | INFO, 18 | WARN, 19 | ERROR 20 | } LogLevel; 21 | 22 | 23 | const char *levelstr(LogLevel level); 24 | void log_msg(LogLevel level, const char *msg, ...); 25 | void log_setlevel(LogLevel level); 26 | LogLevel log_getlevel(); 27 | 28 | 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /p2pchat/message.c: -------------------------------------------------------------------------------- 1 | #include "message.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | const char *strmtype(MessageType type) { 8 | switch(type) { 9 | case MTYPE_LOGIN: return "LOGIN"; 10 | case MTYPE_LOGOUT: return "LOGOUT"; 11 | case MTYPE_LIST: return "LIST"; 12 | case MTYPE_PUNCH: return "PUNCH"; 13 | case MTYPE_PING: return "PING"; 14 | case MTYPE_PONG: return "PONG"; 15 | case MTYPE_REPLY: return "REPLY"; 16 | case MTYPE_TEXT: return "TEXT"; 17 | default: return "UNKNOW"; 18 | } 19 | } 20 | 21 | /* return bytes serialized */ 22 | int msg_pack(Message msg, char *buf, unsigned int bufsize) { 23 | if (bufsize < MSG_HEADLEN + msg.head.length) { 24 | printf("buf too small"); 25 | return 0; 26 | } 27 | int16_t m_magic = htons(msg.head.magic); 28 | int16_t m_type = htons(msg.head.type); 29 | int32_t m_length = htonl(msg.head.length); 30 | int index = 0; 31 | memcpy(buf + index, &m_magic, MSG_MAGICLEN); 32 | index += MSG_MAGICLEN; 33 | memcpy(buf + index, &m_type, MSG_TYPELEN); 34 | index += MSG_TYPELEN; 35 | memcpy(buf + index, &m_length, MSG_BODYLEN); 36 | index += MSG_BODYLEN; 37 | memcpy(buf + index, msg.body, msg.head.length); 38 | index += msg.head.length; 39 | return index; 40 | } 41 | 42 | /* 43 | Message body is a pointer to buf + len(head) 44 | */ 45 | Message msg_unpack(const char *buf, unsigned int buflen) { 46 | Message m; 47 | memset(&m, 0, sizeof(m)); 48 | if (buflen < MSG_HEADLEN) { 49 | // at least we won't get an overflow 50 | return m; 51 | } 52 | int index = 0; 53 | m.head.magic = ntohs(*(uint16_t *)(buf + index)); 54 | index += sizeof(uint16_t); 55 | if (m.head.magic != MSG_MAGIC) { 56 | return m; 57 | } 58 | m.head.type = ntohs(*(uint16_t *)(buf + index)); 59 | index += sizeof(uint16_t); 60 | m.head.length = ntohl(*(uint32_t *)(buf + index)); 61 | index += sizeof(uint32_t); 62 | if (index + m.head.length > buflen) { 63 | printf("message declared body size(%d) is larger than what's received (%d), truncating\n", 64 | m.head.length, buflen - MSG_HEADLEN); 65 | m.head.length = buflen - index; 66 | } 67 | m.body = buf + index; 68 | return m; 69 | } 70 | 71 | // send a Message 72 | int udp_send_msg(int sock, endpoint_t peer, Message msg) { 73 | char buf[SEND_BUFSIZE] = {0}; 74 | int wt_size = msg_pack(msg, buf, SEND_BUFSIZE); 75 | return sendto(sock, buf, wt_size, 76 | MSG_DONTWAIT, (struct sockaddr *)&peer, sizeof(peer)); 77 | } 78 | // send a buf with length 79 | int udp_send_buf(int sock, endpoint_t peer, 80 | MessageType type, const char *buf, unsigned int len) { 81 | Message m; 82 | m.head.magic = MSG_MAGIC; 83 | m.head.type = type; 84 | m.head.length = len; 85 | m.body = buf; 86 | return udp_send_msg(sock, peer, m); 87 | } 88 | // send a NULL terminated text 89 | int udp_send_text(int sock, endpoint_t peer, 90 | MessageType type, const char *text) { 91 | unsigned int len = text == NULL ? 0 : strlen(text); 92 | return udp_send_buf(sock, peer, type, text, len); 93 | } 94 | -------------------------------------------------------------------------------- /p2pchat/message.h: -------------------------------------------------------------------------------- 1 | #ifndef P2PCHAT_MESSAGE_H 2 | #define P2PCHAT_MESSAGE_H 3 | #include 4 | #include "endpoint.h" 5 | 6 | #define MSG_MAGIC 0x8964 7 | #define MSG_MAGICLEN 2 8 | #define MSG_TYPELEN 2 9 | #define MSG_BODYLEN 4 10 | #define MSG_HEADLEN MSG_MAGICLEN + MSG_TYPELEN + MSG_BODYLEN 11 | /* a message is a UDP datagram with following structure: 12 | -----16bits--+---16bits--+-----32bits----------+---len*8bits---+ 13 | -- 0x8964 + msg type + msg length(exclude) + message body + 14 | -------------+-----------+---------------------+---------------+ 15 | */ 16 | #define SEND_BUFSIZE 1024 17 | #define RECV_BUFSIZE 1024 18 | typedef enum _MessageType MessageType; 19 | typedef struct _Message Message; 20 | typedef struct _MessageHead MessageHead; 21 | enum _MessageType { 22 | MTYPE_LOGIN = 0, 23 | MTYPE_LOGOUT, 24 | MTYPE_LIST, 25 | MTYPE_PUNCH, 26 | MTYPE_PING, 27 | MTYPE_PONG, 28 | MTYPE_REPLY, 29 | MTYPE_TEXT, 30 | MTYPE_END 31 | }; 32 | 33 | struct _MessageHead { 34 | uint16_t magic; 35 | uint16_t type; 36 | uint32_t length; 37 | }__attribute__((packed)); 38 | 39 | struct _Message { 40 | MessageHead head; 41 | const char *body; 42 | }; 43 | 44 | const char *strmtype(MessageType type); 45 | int msg_pack(Message msg, char *buf, unsigned int bufsize); 46 | Message msg_unpack(const char *buf, unsigned int bufsize); 47 | 48 | // replay a Message 49 | int udp_send_msg(int sock, endpoint_t peer, Message msg); 50 | // reply a buf with length 51 | int udp_send_buf(int sock, endpoint_t peer, MessageType type, 52 | const char *buf, unsigned int len); 53 | // reply a NULL terminated text 54 | int udp_send_text(int sock, endpoint_t peer, MessageType type, const char *text); 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /p2pchat/server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "message.h" 8 | #include "logging.h" 9 | #include "endpoint.h" 10 | #include "endpoint_list.h" 11 | 12 | #define MAX_CLIENTS 10 13 | typedef void callback_t(int server_sock, endpoint_t from, Message msg); 14 | eplist_t *g_client_pool; 15 | 16 | void udp_receive_loop(int listen_sock, callback_t callback) 17 | { 18 | endpoint_t peer; 19 | socklen_t addrlen; 20 | char buf[RECV_BUFSIZE]; 21 | for(;;) { 22 | addrlen = sizeof(peer); 23 | memset(&peer, 0, addrlen); 24 | memset(buf, 0, RECV_BUFSIZE); 25 | int rd_size; 26 | /* UDP isn't a "stream" protocol. once you do the initial recvfrom, 27 | the remainder of the packet is discarded */ 28 | rd_size = recvfrom(listen_sock, buf, RECV_BUFSIZE, 0, 29 | (struct sockaddr*)&peer, &addrlen); 30 | if (rd_size == -1) { 31 | perror("recvfrom"); 32 | break; 33 | } else if (rd_size == 0) { 34 | log_info("EOF from %s", ep_tostring(peer)); 35 | continue; 36 | } 37 | Message msg = msg_unpack(buf, rd_size); 38 | if (msg.head.magic != MSG_MAGIC || msg.body == NULL) { 39 | log_warn("Invalid message(%d bytes): {0x%x,%d,%d} %p", rd_size, 40 | msg.head.magic, msg.head.type, msg.head.length, msg.body); 41 | continue; 42 | } 43 | callback(listen_sock, peer, msg); 44 | continue; 45 | 46 | } 47 | log_info("udp_receive_loop stopped."); 48 | } 49 | 50 | void on_message(int sock, endpoint_t from, Message msg) { 51 | log_debug("RECV %d bytes FROM %s: %s %s", msg.head.length, 52 | ep_tostring(from), strmtype(msg.head.type), msg.body); 53 | switch(msg.head.type) { 54 | case MTYPE_LOGIN: 55 | { 56 | if (0 == eplist_add(g_client_pool, from)) { 57 | log_info("%s logged in", ep_tostring(from)); 58 | udp_send_text(sock, from, MTYPE_REPLY, "Login success!"); 59 | } else { 60 | log_warn("%s failed to login", ep_tostring(from)); 61 | udp_send_text(sock, from, MTYPE_REPLY, "Login failed"); 62 | } 63 | } 64 | break; 65 | case MTYPE_LOGOUT: 66 | { 67 | if (0 == eplist_remove(g_client_pool, from)) { 68 | log_info("%s logged out", ep_tostring(from)); 69 | udp_send_text(sock, from, MTYPE_REPLY, "Logout success"); 70 | } else { 71 | log_info("%s failed to logout", ep_tostring(from)); 72 | udp_send_text(sock, from, MTYPE_REPLY, "Logout failed"); 73 | } 74 | } 75 | break; 76 | case MTYPE_LIST: 77 | { 78 | log_info("%s quering list", ep_tostring(from)); 79 | char text[SEND_BUFSIZE - MSG_HEADLEN] = {0}; 80 | for (eplist_t *c = g_client_pool->next; c != NULL; c = c->next) { 81 | if (ep_equal(c->endpoint, from)) strcat(text, "(you)"); 82 | strcat(text, ep_tostring(c->endpoint)); 83 | if (c->next) strcat(text, ";"); 84 | } 85 | udp_send_text(sock, from, MTYPE_REPLY, text); 86 | } 87 | break; 88 | case MTYPE_PUNCH: 89 | { 90 | endpoint_t other = ep_fromstring(msg.body); 91 | log_info("punching to %s", ep_tostring(other)); 92 | udp_send_text(sock, other, MTYPE_PUNCH, ep_tostring(from)); 93 | udp_send_text(sock, from, MTYPE_TEXT, "punch request sent"); 94 | } 95 | break; 96 | case MTYPE_PING: 97 | udp_send_text(sock, from, MTYPE_PONG, NULL); 98 | break; 99 | case MTYPE_PONG: 100 | break; 101 | default: 102 | udp_send_text(sock, from, MTYPE_REPLY, "Unkown command"); 103 | break; 104 | } 105 | } 106 | 107 | int main(int argc, char **argv) { 108 | log_setlevel(DEBUG); 109 | if (argc != 2) { 110 | printf("Usage: %s \n", argv[0]); 111 | return 1; 112 | } 113 | const char *host = "0.0.0.0"; 114 | int port = atoi(argv[1]); 115 | int ret; 116 | endpoint_t server = ep_frompair(host, port); 117 | 118 | int sock = socket(AF_INET, SOCK_DGRAM, 0); 119 | if (sock == -1) { 120 | perror("socket"); 121 | exit(EXIT_FAILURE); 122 | } 123 | ret = bind(sock, (const struct sockaddr*)&server, sizeof(server)); 124 | if (ret == -1) { 125 | perror("bind"); 126 | exit(EXIT_FAILURE); 127 | } 128 | g_client_pool = eplist_create(); 129 | 130 | log_info("server start on %s", ep_tostring(server)); 131 | udp_receive_loop(sock, on_message); 132 | 133 | eplist_destroy(g_client_pool); 134 | return EXIT_SUCCESS; 135 | } 136 | -------------------------------------------------------------------------------- /p2pchat/tests/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS := -g -Wall -Wno-implicit-function-declaration 2 | TARGETS := test_logging test_endpoint test_message test_eplist 3 | all: $(TARGETS) 4 | 5 | test_logging: test_logging.c ../logging.c 6 | gcc $^ -o $@ $(CFLAGS) 7 | test_endpoint: test_endpoint.c ../endpoint.c 8 | gcc $^ -o $@ $(CFLAGS) 9 | test_message: test_message.c ../message.c 10 | gcc $^ -o $@ $(CFLAGS) 11 | test_eplist: test_eplist.c ../endpoint.c ../endpoint_list.c 12 | gcc $^ -o $@ $(CFLAGS) 13 | clean: 14 | rm -rf $(TARGETS) 15 | .PHONY: clean 16 | -------------------------------------------------------------------------------- /p2pchat/tests/test_endpoint.c: -------------------------------------------------------------------------------- 1 | #include "../endpoint.h" 2 | #include 3 | #include 4 | static void test_pod() { 5 | endpoint_t ep = ep_fromstring("127.0.0.1:1234"); 6 | assert(ep.sin_family == AF_INET); 7 | assert(ep.sin_addr.s_addr == inet_addr("127.0.0.1")); 8 | assert(ep.sin_port == htons(1234)); 9 | 10 | endpoint_t ep1 = ep; 11 | assert(ep_equal(ep1, ep)); 12 | ep1.sin_port = 1234; 13 | assert(!ep_equal(ep1, ep)); 14 | printf("test_pod pass\n"); 15 | } 16 | 17 | static void test_convert() { 18 | endpoint_t ep = ep_fromstring("127.0.0.1:1234"); 19 | endpoint_t ep1 = ep_frompair("127.0.0.1", 1234); 20 | assert(ep_equal(ep1, ep)); 21 | char *str = ep_tostring(ep1); 22 | assert(strcmp(str, "127.0.0.1:1234") == 0); 23 | printf("test_convert pass\n"); 24 | } 25 | 26 | static void test_error() { 27 | endpoint_t ep; 28 | const char *testcase[] = { 29 | "", 30 | "xxxxx", 31 | "xxx:0", 32 | "xxx:yyy", 33 | NULL 34 | }; 35 | for (int i = 0; testcase[i] ; i++) { 36 | ep = ep_fromstring(testcase[i]); 37 | assert(strcmp("255.255.255.255:0", ep_tostring(ep)) == 0); 38 | } 39 | printf("test_error pass\n"); 40 | } 41 | int main() 42 | { 43 | test_pod(); 44 | test_convert(); 45 | test_error(); 46 | return 0; 47 | } 48 | -------------------------------------------------------------------------------- /p2pchat/tests/test_eplist.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../endpoint.h" 4 | #include "../endpoint_list.h" 5 | /* 6 | * Test with valgrind 7 | * */ 8 | void test_basic() { 9 | eplist_t *pool = eplist_create(); 10 | assert(0 == eplist_count(pool)); 11 | 12 | eplist_add(pool, ep_fromstring("192.168.0.1:3389")); 13 | eplist_add(pool, ep_fromstring("127.0.0.1:1234")); 14 | eplist_add(pool, ep_fromstring("127.0.0.1:3389")); 15 | 16 | printf("=== 3 ===\n"); 17 | assert(3 == eplist_count(pool)); 18 | eplist_dump(pool); 19 | 20 | // remove center 21 | eplist_remove(pool, ep_fromstring("127.0.0.1:1234")); 22 | printf("=== 2 ===\n"); 23 | assert(2 == eplist_count(pool)); 24 | eplist_dump(pool); 25 | 26 | // remove head 27 | eplist_remove(pool, ep_fromstring("192.168.0.1:3389")); 28 | printf("=== 1 ===\n"); 29 | assert(1 == eplist_count(pool)); 30 | eplist_dump(pool); 31 | 32 | // remove tail 33 | eplist_remove(pool, ep_fromstring("127.0.0.1:3389")); 34 | printf("=== 0 ===\n"); 35 | assert(0 == eplist_count(pool)); 36 | eplist_dump(pool); 37 | 38 | eplist_destroy(pool); 39 | printf("test_basic pass\n"); 40 | } 41 | 42 | void test_corner() { 43 | eplist_t *pool = NULL; 44 | assert (-1 == eplist_count(pool)); 45 | pool = eplist_create(); 46 | assert(0 == eplist_add(pool, ep_fromstring("0:80"))); 47 | assert(0 != eplist_add(pool, ep_fromstring("0:80"))); 48 | assert(1 == eplist_count(pool)); 49 | printf("test_corner pass\n"); 50 | eplist_destroy(pool); 51 | } 52 | int main() 53 | { 54 | test_basic(); 55 | test_corner(); 56 | return 0; 57 | } 58 | -------------------------------------------------------------------------------- /p2pchat/tests/test_logging.c: -------------------------------------------------------------------------------- 1 | #include "../logging.h" 2 | 3 | int main() { 4 | int i = 0; 5 | log_setlevel(DEBUG); 6 | log_debug("hello %d", i++); 7 | log_info("hello %d", i++); 8 | log_warn("hello %d", i++); 9 | log_err("hello %d", i++); 10 | log_setlevel(INFO); 11 | log_debug("hello %d", i++); 12 | log_info("hello %d", i++); 13 | log_warn("hello %d", i++); 14 | log_err("hello %d", i++); 15 | log_setlevel(WARN); 16 | log_debug("hello %d", i++); 17 | log_info("hello %d", i++); 18 | log_warn("hello %d", i++); 19 | log_err("hello %d", i++); 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /p2pchat/tests/test_message.c: -------------------------------------------------------------------------------- 1 | #include "../message.h" 2 | #include 3 | #include 4 | 5 | static void test_mtype() { 6 | unsigned int len = sizeof(unsigned int); 7 | assert(len == 4); 8 | assert(len == sizeof(MessageType)); 9 | assert(len == sizeof(MTYPE_LOGIN)); 10 | assert(len == sizeof(MTYPE_TEXT)); 11 | assert(len == sizeof(MTYPE_END)); 12 | printf("test_type pass\n"); 13 | } 14 | static void test_msg() { 15 | assert(8 == sizeof(MessageHead)); 16 | assert(8+8 == sizeof(Message)); 17 | MessageHead head; 18 | Message m1, m2; 19 | 20 | head.magic = 0x8964; 21 | head.type = 2; 22 | head.length = 4; 23 | m1.head = head; 24 | m1.body = "hello"; 25 | m2 = m1; 26 | printf("m1: {0x%x,%d,%d} %p\n", 27 | m1.head.magic, m1.head.type, m1.head.length, 28 | m1.body); 29 | printf("m2: {0x%x,%d,%d} %p\n", 30 | m2.head.magic, m2.head.type, m2.head.length, 31 | m2.body); 32 | assert(m2.head.magic == 0x8964); 33 | assert(m2.head.type == 2); 34 | assert(m2.head.length == 4); 35 | // the pointer is copied 36 | assert((long)m2.body == (long)m1.body); 37 | printf("test_msg pass\n"); 38 | } 39 | int main() 40 | { 41 | test_mtype(); 42 | test_msg(); 43 | return 0; 44 | } 45 | -------------------------------------------------------------------------------- /run_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | testcases=$(find p2pchat/tests -type f -name "test_*" -executable) 4 | if test -z $testcase;then 5 | echo "No test found. Please run \`make test' first" 6 | exit 1 7 | fi 8 | 9 | echo "Collected $(wc -l <<< $testcases) tests" 10 | passed=0 11 | failed=0 12 | for testcase in $testcases ;do 13 | $testcase 14 | if [ $? -eq 0 ];then 15 | ret='[PASS]' 16 | passed=$(($passed + 1)) 17 | else 18 | ret='[FAIL]' 19 | failed=$(($failed + 1)) 20 | fi 21 | echo "$ret $testcase" 22 | done 23 | echo "Done. $passed pass, $failed failed." 24 | -------------------------------------------------------------------------------- /stun/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Classic STUN ([RFC3489][rfc3489]) 3 | 4 | The classic STUN datagram structure is as follow(TLV encoded): 5 | All STUN messages consist of a 20 byte header: 6 | 7 | ``` 8 | 0 1 2 3 9 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 10 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 11 | | STUN Message Type | Message Length | 12 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 13 | | 14 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 15 | 16 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 17 | Transaction ID 18 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 19 | | 20 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 21 | ``` 22 | 23 | After the header are 0 or more attributes. Each attribute is TLV 24 | encoded, with a 16 bit type, 16 bit length, and variable value: 25 | 26 | ``` 27 | 0 1 2 3 28 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 29 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 30 | | Type | Length | 31 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 32 | | Value .... 33 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 34 | ``` 35 | 36 | 37 | 38 | ``` 39 | Figure 2: Flow for type discovery process 40 | 41 | +--------+ 42 | | Test | 43 | | I | 44 | +--------+ 45 | | 46 | | 47 | V 48 | /\ /\ 49 | N / \ Y / \ Y +--------+ 50 | UDP <-------/Resp\--------->/ IP \------------->| Test | 51 | Blocked \ ? / \Same/ | II | 52 | \ / \? / +--------+ 53 | \/ \/ | 54 | | N | 55 | | V 56 | V /\ 57 | +--------+ Sym. N / \ 58 | | Test | UDP <---/Resp\ 59 | | II | Firewall \ ? / 60 | +--------+ \ / 61 | | \/ 62 | V |Y 63 | /\ /\ | 64 | Symmetric N / \ +--------+ N / \ V 65 | NAT <--- / IP \<-----| Test |<--- /Resp\ Open 66 | \Same/ | I | \ ? / Internet 67 | \? / +--------+ \ / 68 | \/ \/ 69 | | |Y 70 | | | 71 | | V 72 | | Full 73 | | Cone 74 | V /\ 75 | +--------+ / \ Y 76 | | Test |------>/Resp\---->Restricted 77 | | III | \ ? / 78 | +--------+ \ / 79 | \/ 80 | |N 81 | | Port 82 | +------>Restricted 83 | ``` 84 | 85 | 86 | [rfc3489]:https://www.ietf.org/rfc/rfc3489.txt 87 | -------------------------------------------------------------------------------- /stun/classic_stun_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import io 3 | import sys 4 | import struct 5 | import socket 6 | import logging 7 | from enum import Enum, unique 8 | from random import randint 9 | 10 | @unique 11 | class MessageType(Enum): 12 | BINDING_REQUEST = 0x0001 13 | BINDING_RESPONSE = 0x0101 14 | BINDING_ERROR_RESPONSE = 0x0111 15 | SHARED_SECRET_REQUEST = 0x0002 16 | SHARED_SECRET_RESPONSE = 0x0102 17 | SHARED_SECRET_ERROR = 0x0112 18 | 19 | @unique 20 | class AttributeType(Enum): 21 | MAPPED_ADDRESS = 0x0001 22 | RESPONSE_ADDRESS = 0x0002 23 | CHANGE_REQUEST = 0x0003 24 | SOURCE_ADDRESS = 0x0004 25 | CHANGED_ADDRESS = 0x0005 26 | USERNAME = 0x0006 27 | PASSWORD = 0x0007 28 | MESSAGE_INTEGRITY = 0x0008 29 | ERROR_CODE = 0x0009 30 | UNKNOWN_ATTRIBUTES = 0x000a 31 | REFLECTED_FROM = 0x000b 32 | XOR_MAPPED_ADDRESS = 0x8020 33 | SERVER = 0x8022 34 | SECONDARY_ADDRESS = 0x8050 35 | 36 | @unique 37 | class NAT(Enum): 38 | PUBLIC = 'The open Internet' 39 | UDP_BLOCKED = 'Firewall that blocks UDP' 40 | SYMMETRIC_UDP_FIREWALL = 'Firewall that allows UDP out, and responses have to come back to the source of the request' 41 | FULL_CONE = 'Full Cone NAT' 42 | SYMMETRIC = 'Symmetric NAT' 43 | PORT_RISTRICT = 'Port Rristrict Cone NAT' 44 | ADDR_RISTRICT = '(Address) Rristrict Cone NAT' 45 | 46 | class StunHeader(object): 47 | """ 20 bytes header """ 48 | def __init__(self, **kwargs): 49 | # 16 bits 50 | self.type = kwargs.pop('type', None) 51 | # 16 bits body length(excluding 20 bytes header) 52 | self.length = kwargs.pop('length', 0) 53 | # 128 bits 54 | self.transactionId = kwargs.pop('transactionId', randint(0, (1 << 128) - 1)) 55 | if len(kwargs) != 0: 56 | raise ValueError('unknown kwargs: {}'.format(kwargs)) 57 | def to_bytes(self): 58 | return struct.pack('!HH', self.type.value, self.length) + \ 59 | self.transactionId.to_bytes(16, 'big') 60 | @classmethod 61 | def from_bytes(cls, data): 62 | assert len(data) == 20 63 | _type, _len, _tid = struct.unpack('!HH16s', data) 64 | return cls( 65 | type=MessageType(_type), 66 | length=_len, 67 | transactionId = int.from_bytes(_tid, 'big')) 68 | def __str__(self): 69 | return '<{}|{}|{:X}>'.format( 70 | self.type.name if self.type else None, 71 | self.length, self.transactionId) 72 | 73 | class StunAttribute(object): 74 | HEADER_LENGTH = 4 75 | def __init__(self, **kwargs): 76 | self.type = kwargs.pop('type', None) 77 | self.length = kwargs.pop('length', 0) 78 | self.value = kwargs.pop('value', b'') 79 | if len(kwargs) != 0: 80 | raise ValueError('unknown param: {}'.format(kwargs)) 81 | @classmethod 82 | def change_request(cls, change_addr=False, change_port=False): 83 | change_addr = '1' if change_addr else '0' 84 | change_port = '1' if change_addr else '0' 85 | # padding is unnecessary 86 | v = int('0' * 29 + change_addr + change_port + '0', 2) 87 | _binary = struct.pack('!I', v) 88 | return cls(type=AttributeType.CHANGE_REQUEST, 89 | length=len(_binary), 90 | value=_binary) 91 | 92 | def to_bytes(self): 93 | self.length = len(self.value) 94 | return struct.pack('!HH', self.type.value, self.length) + self.value 95 | def is_address(self): 96 | return self.length == 8 and self.type in [ 97 | AttributeType.MAPPED_ADDRESS, 98 | AttributeType.RESPONSE_ADDRESS, 99 | AttributeType.CHANGED_ADDRESS] 100 | @property 101 | def address(self): 102 | if self.is_address(): 103 | _, _family, port, ip = struct.unpack('!cBHI', self.value) 104 | return socket.inet_ntoa(struct.pack('!I', ip)), port 105 | def __str__(self): 106 | if self.is_address(): 107 | return ''.format(self.type.name, 108 | self.address[0], self.address[1]) 109 | else: 110 | return ''.format(self.type.name if self.type else None) 111 | 112 | class Message(object): 113 | def __init__(self, **kwargs): 114 | self.header = kwargs.pop('header', None) 115 | self.attributes = kwargs.pop('attributes', []) 116 | def to_bytes(self): 117 | # network order (big endian) 118 | _header = b'' 119 | _body = b'' 120 | for attr in self.attributes: 121 | _body += attr.to_bytes() 122 | self.header.length = len(_body) 123 | _header = self.header.to_bytes() 124 | return _header + _body 125 | @classmethod 126 | def from_bytes(cls, data): 127 | header = StunHeader.from_bytes(data[:20]) 128 | attributes = [] 129 | datalen = header.length 130 | f = io.BytesIO(data[20:]) 131 | while datalen > 0: 132 | _type, _len = struct.unpack('!HH', f.read(StunAttribute.HEADER_LENGTH)) 133 | _value = f.read(_len) 134 | attributes.append(StunAttribute( 135 | type=AttributeType(_type), 136 | length=_len, 137 | value=_value)) 138 | datalen -= StunAttribute.HEADER_LENGTH + _len 139 | return cls(header=header, attributes=attributes) 140 | def __str__(self): 141 | return '{}: [{}]'.format(self.header, 142 | ','.join(map(str, self.attributes))) 143 | 144 | def send_and_recv(sock, stun_server, request): 145 | logging.debug('SEND: {}'.format(request)) 146 | sock.sendto(request.to_bytes(), stun_server) 147 | try: 148 | data, addr = sock.recvfrom(4096) 149 | except socket.timeout as e: 150 | logging.debug('RECV: timeout') 151 | return None 152 | response = Message.from_bytes(data) 153 | logging.debug('RECV: {}'.format(response)) 154 | return response 155 | 156 | def test_I(sock, stun_server): 157 | logging.info('running test I with {}:{}'.format(stun_server[0], stun_server[1])) 158 | binding_request = Message(header=StunHeader(type=MessageType.BINDING_REQUEST)) 159 | return send_and_recv(sock, stun_server, binding_request) 160 | 161 | def test_II(sock, stun_server): 162 | logging.info('running test II with {}:{}'.format(stun_server[0], stun_server[1])) 163 | binding_request = Message(header=StunHeader(type=MessageType.BINDING_REQUEST)) 164 | change = StunAttribute.change_request(True, True) 165 | binding_request.attributes.append(change) 166 | return send_and_recv(sock, stun_server, binding_request) 167 | 168 | def test_III(sock, stun_server): 169 | logging.info('running test III with {}:{}'.format(stun_server[0], stun_server[1])) 170 | binding_request = Message(header=StunHeader(type=MessageType.BINDING_REQUEST)) 171 | change = StunAttribute.change_request(False, True) 172 | binding_request.attributes.append(change) 173 | return send_and_recv(sock, stun_server, binding_request) 174 | 175 | def get_mapped_address(message): 176 | for attr in message.attributes: 177 | if attr.type is AttributeType.MAPPED_ADDRESS: 178 | return attr.address 179 | def get_changed_address(message): 180 | for attr in message.attributes: 181 | if attr.type is AttributeType.CHANGED_ADDRESS: 182 | return attr.address 183 | def test_nat(sock, stun_server, local_ip='0.0.0.0'): 184 | # Please refer to the README 185 | resp = test_I(sock, stun_server) 186 | if resp is None: 187 | return NAT.UDP_BLOCKED 188 | local_address = local_ip, sock.getsockname()[1] 189 | logging.info('local address is {}:{}'.format(local_address[0], local_address[1])) 190 | m1 = get_mapped_address(resp) 191 | changed_address = get_changed_address(resp) 192 | if m1 == local_address: 193 | # we can't tell whether it's public if we don't specify the local address 194 | resp = test_II(sock, stun_server) 195 | if resp is None: 196 | return NAT.SYMMETRIC_UDP_FIREWALL 197 | return NAT.PUBLIC 198 | logging.info('MAPPED_ADDRESS: {}:{}'.format(m1[0], m1[1])) 199 | resp = test_II(sock, stun_server) 200 | if not resp is None: 201 | return NAT.FULL_CONE 202 | resp = test_I(sock, changed_address) 203 | assert not (resp is None) 204 | m2 = get_mapped_address(resp) 205 | logging.info('MAPPED_ADDRESS: {}:{}'.format(m2[0], m2[1])) 206 | if m2 != m1: 207 | return NAT.SYMMETRIC 208 | resp = test_III(sock, stun_server) 209 | if resp is None: 210 | return NAT.PORT_RISTRICT 211 | else: 212 | return NAT.ADDR_RISTRICT 213 | 214 | STUN_SERVERS = [ 215 | ('stun.pppan.net', 3478), 216 | ('stun.ekiga.net', 3478), 217 | ('stun.ideasip.com', 3478), 218 | ('stun.voipbuster.com', 3478), 219 | ] 220 | 221 | def main(): 222 | if len(sys.argv) == 2: 223 | local_ip = sys.argv[1] 224 | else: 225 | local_ip = '0.0.0.0' 226 | logging.basicConfig(level=logging.INFO) 227 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 228 | sock.settimeout(3.0) 229 | # choose the fastest stun server to you 230 | ntype = test_nat(sock, STUN_SERVERS[0], local_ip) 231 | print('NAT_TYPE: ' + ntype.value) 232 | 233 | if __name__ == '__main__': 234 | main() 235 | -------------------------------------------------------------------------------- /tools/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS := -g -std=c99 -Wall -Wno-implicit-function-declaration 2 | TARGETS := udp_server udp_client 3 | all: $(TARGETS) 4 | 5 | udp_server: udp_server.c 6 | gcc $^ -o $@ $(CFLAGS) 7 | udp_client: udp_client.c 8 | gcc $^ -o $@ $(CFLAGS) -lpthread 9 | clean: 10 | rm -rf $(TARGETS) 11 | .PHONY: clean 12 | 13 | -------------------------------------------------------------------------------- /tools/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals 4 | from __future__ import print_function, absolute_import 5 | 6 | import sys 7 | import struct 8 | import socket 9 | 10 | def serialize(type, data): 11 | data = data.encode('utf-8') 12 | #return struct.pack('!H', 0x8964)\ 13 | # + struct.pack('!H', type)\ 14 | # + struct.pack('!I', len(data))\ 15 | # + data 16 | return struct.pack('!HHI{}s'.format(len(data)), 0x8964, type, len(data), data) 17 | 18 | def deserialize(buf): 19 | head = 8 20 | m_magic, m_type, m_length = struct.unpack('!HHI', buf[:head]) 21 | m_data = buf[head: head+m_length] 22 | return '(0x{:x}, {}, {}): {}'.format(m_magic, m_type, m_length, m_data.decode('utf-8')) 23 | 24 | def main(): 25 | if len(sys.argv) < 2: 26 | print('Usage %s type [text...]' % sys.argv[0]) 27 | return 28 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 29 | server = ('127.0.0.1', 9999) 30 | res = sock.sendto(serialize(int(sys.argv[1]), ' '.join(sys.argv[2:])), server) 31 | data, addr = sock.recvfrom(1024) 32 | print('{}: {}'.format(addr, deserialize(data))) 33 | 34 | if __name__ == '__main__': 35 | main() 36 | -------------------------------------------------------------------------------- /tools/udp_client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #define RECV_BUFSIZE 512 10 | void *udp_receive_loop(void *sockfd) 11 | { 12 | struct sockaddr_in addr; 13 | socklen_t addrlen; 14 | char data[RECV_BUFSIZE]; 15 | for(;;) { 16 | addrlen = sizeof(addr); 17 | memset(&addr, 0, addrlen); 18 | memset(data, 0, RECV_BUFSIZE); 19 | int rd_size = recvfrom(*(int*)sockfd, data, RECV_BUFSIZE, 0, 20 | (struct sockaddr*)&addr, &addrlen); 21 | if (rd_size == -1) { 22 | perror("recvfrom"); 23 | break; 24 | } 25 | char *ip = inet_ntoa(addr.sin_addr); 26 | int port = addr.sin_port; 27 | if (rd_size == 0) { 28 | printf("disconnect from %s:%d\n", ip, port); 29 | continue; 30 | } 31 | printf("recv %d bytes from [%s:%d]: %s\n", 32 | rd_size, ip, port, data); 33 | } 34 | printf("udp_receive_loop stopped.\n"); 35 | return 0; 36 | } 37 | int udp_send(int sockfd, 38 | const char *ip, int port, 39 | const char *data, int data_len) 40 | { 41 | struct sockaddr_in addr; 42 | socklen_t addrlen = sizeof(addr); 43 | memset(&addr, 0, addrlen); 44 | addr.sin_family = AF_INET; 45 | addr.sin_addr.s_addr = inet_addr(ip); 46 | addr.sin_port = htons(port); 47 | ssize_t sent = sendto(sockfd, data, data_len, MSG_DONTWAIT, 48 | (const struct sockaddr *)&addr, addrlen); 49 | printf("Sent %zd bytes to %s:%d\n", sent, ip, port); 50 | return sent; 51 | } 52 | void print_help() 53 | { 54 | const char *help_message = "" 55 | "Usage:" 56 | "\n\n sendto host:port data" 57 | "\n Send text [data] to [host:port] through UDP protocol." 58 | "\n Example:" 59 | "\n >>> sendto 114.114.114.114:53 hello" 60 | "\n\n help" 61 | "\n Print this help message." 62 | "\n\n quit" 63 | "\n Quit this program."; 64 | printf("%s\n", help_message); 65 | } 66 | 67 | void console_loop(int sockfd) 68 | { 69 | char *line = NULL; 70 | size_t len; 71 | ssize_t read; 72 | while(fprintf(stdout, ">>> ") && (read = getline(&line, &len, stdin)) != -1) 73 | { 74 | if (read == 1) 75 | continue; 76 | char *cmd = strtok(line, " "); 77 | if (strncmp(cmd, "sendto", 5) == 0) { 78 | char *host_port = strtok(NULL, " "); 79 | char *data = strtok(NULL, " "); 80 | 81 | char *host = strtok(host_port, ":"); 82 | char *s_port = strtok(NULL, ":"); 83 | int port = atoi(s_port); 84 | udp_send(sockfd, host, port, data, strlen(data)); 85 | } else if (strncmp(cmd, "quit", 4) == 0) { 86 | printf("Quiting...\n"); 87 | break; 88 | } else { 89 | printf("Unknown command %s\n", cmd); 90 | print_help(); 91 | } 92 | } 93 | free(line); 94 | } 95 | int main() 96 | { 97 | int sock = socket(AF_INET, SOCK_DGRAM, 0); 98 | if (sock == -1) { 99 | perror("socket"); 100 | } 101 | pthread_t pid; 102 | pthread_create(&pid, NULL, &udp_receive_loop ,&sock); 103 | console_loop(sock); 104 | close(sock); 105 | return 0; 106 | } 107 | 108 | -------------------------------------------------------------------------------- /tools/udp_server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define RECV_BUFSIZE 512 10 | void udp_server_loop(int listen_sock) 11 | { 12 | struct sockaddr_in addr; 13 | socklen_t addrlen; 14 | char data[RECV_BUFSIZE]; 15 | for(;;) { 16 | addrlen = sizeof(addr); 17 | memset(&addr, 0, addrlen); 18 | memset(data, 0, RECV_BUFSIZE); 19 | int rd_size = recvfrom(listen_sock, data, RECV_BUFSIZE, 0, 20 | (struct sockaddr*)&addr, &addrlen); 21 | if (rd_size == -1) { 22 | perror("recvfrom"); 23 | break; 24 | } 25 | char *ip = inet_ntoa(addr.sin_addr); 26 | int port = addr.sin_port; 27 | if (rd_size == 0) { 28 | printf("disconnect from %s:%d\n", ip, port); 29 | continue; 30 | } 31 | printf("recv %d bytes from [%s:%d]: %s\n", 32 | rd_size, ip, port, data); 33 | } 34 | printf("udp_receive_loop stopped.\n"); 35 | } 36 | int main(int argc, char **argv) { 37 | if (argc != 2) { 38 | printf("Usage: %s \n", argv[0]); 39 | return 1; 40 | } 41 | const char *host = "0.0.0.0"; 42 | int port = atoi(argv[1]); 43 | struct sockaddr_in addr; 44 | socklen_t addrlen = sizeof(addr); 45 | memset(&addr, 0, addrlen); 46 | addr.sin_family = AF_INET; 47 | addr.sin_port = htons(port); 48 | addr.sin_addr.s_addr = inet_addr(host); 49 | 50 | int sock = socket(AF_INET, SOCK_DGRAM, 0); 51 | if (sock == -1) { 52 | perror("socket"); 53 | } 54 | int ret = bind(sock, (struct sockaddr*)&addr, addrlen); 55 | if (ret == -1) { 56 | perror("bind"); 57 | } 58 | //ret = listen(sock, 5); 59 | //if (ret == -1) { 60 | // perror("listen"); 61 | //} 62 | printf("UDP bind on %s:%d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); 63 | udp_server_loop(sock); 64 | return 0; 65 | } 66 | --------------------------------------------------------------------------------