1 module natop.natpmp;
2 
3 import natop.network;
4 import natop.exceptions;
5 
6 import std.bitmanip : nativeToBigEndian, bigEndianToNative;
7 import std.conv;
8 import std.exception;
9 import std.random;
10 import std.datetime;
11 
12 import memutils.vector;
13 
14 import vibe.core.core;
15 import vibe.core.net;
16 import vibe.core.log;
17 
18 class NATPMP : Router {
19 private:	
20 	Vector!ubyte m_bytes;
21 	UDPConnection m_udp;
22 	bool m_hasDevice;
23 	IPRoute m_iproute;
24 
25 public:
26 	@property string id() const {
27 		return m_iproute.gateway.toAddressString();
28 	}
29 
30 	@property bool hasDevice() {
31 		return m_hasDevice;
32 	}
33 
34 	this(IPRoute iproute) {
35 		m_iproute = iproute;
36 		bool has_error;
37 		int retries;
38 		do {
39 			
40 			has_error = false;
41 			ushort port = uniform(2000,65000).to!ushort;
42 			try { 
43 				m_udp = listenUDP(port, iproute.destination.toAddressString());
44 			}
45 			catch (Exception e) {
46 				logError("Could not bind to port [%d]: %s", port, e.msg);
47 				has_error = true;
48 				sleep(100.msecs);
49 			}
50 			enforce(++retries < 2);
51 		} while (has_error);
52 		m_udp.connect(iproute.gateway.toAddressString(), 5351);
53 	}
54 
55 	~this() {
56 		try m_udp.close(); catch (Exception e) {
57 			logError("Error in NAT-PMP Dtor: %s", e.msg);
58 		}
59 	}
60 
61 	void discover() {
62 		int retries;
63 		// send a request and watch for error
64 		while (++retries < 2) {
65 			ushort trial_port = cast(ushort) uniform(10000,63000);
66 			ubyte[12] buf = buildRequest(trial_port, true);
67 			m_udp.send(buf.ptr[0 .. 12]);
68 			try readResponse();
69 			catch (NATPMPException e) {
70 				logError("Error in NATPMP discover: %s", e.msg);
71 				return;
72 			}
73 			catch (TimeoutException e) {
74 				logError("Timeout in NATPMP discover: %s", e.msg);
75 				continue;
76 			}
77 			catch (Exception e) {
78 				logError("Generic Error in NATPMP discover: %s", e.msg);
79 				return;
80 			}
81 
82 			m_hasDevice = true;
83 
84 			// remove dummy port redirect
85 			try {
86 				buf = buildRequest(trial_port, true, false);
87 				m_udp.send(buf.ptr[0 .. 12]);
88 				readResponse();
89 			} catch (Exception e) {
90 				logError("Failed to remove dummy port redirect: %s", e.msg);
91 			}
92 			return;
93 		}
94 	}
95 
96 	void createMapping(ushort local_port, ushort external_port, bool is_tcp = true) {
97 		ubyte[12] buf = buildRequest(external_port, is_tcp);
98 		m_udp.send(buf.ptr[0 .. 12]);
99 		readResponse();
100 	}
101 
102 	void deleteMapping(ushort external_port, bool is_tcp = true) {
103 
104 		ubyte[12] buf = buildRequest(external_port, is_tcp, false);
105 		m_udp.send(buf.ptr[0 .. 12]);
106 		readResponse();
107 	}
108 private:
109 	ubyte[12] buildRequest(ushort external_port, bool is_tcp, bool adding = true) {
110 		ubyte[12] buf;
111 		size_t pos;
112 
113 		buf[pos .. pos+1] = nativeToBigEndian!ubyte(0); ++pos;
114 		buf[pos .. pos+1] = nativeToBigEndian!ubyte(is_tcp?2:1); ++pos;
115 		buf[pos .. pos+2] = nativeToBigEndian!ushort(0); pos+=2;
116 		buf[pos .. pos+2] = nativeToBigEndian!ushort(external_port); pos+=2;
117 		buf[pos .. pos+2] = nativeToBigEndian!ushort(external_port); pos+=2;
118 		buf[pos .. pos+4] = nativeToBigEndian!uint(adding?3600:0);
119 
120 		return buf;
121 	}
122 
123 	void readResponse() {
124 		size_t pos;
125 		ubyte[16] data;
126 		m_udp.recv(1.seconds, data.ptr[0 .. 16]);
127 		ubyte version_ = bigEndianToNative!ubyte(*cast(ubyte[1]*)(data.ptr+pos)); ++pos;
128 		ubyte cmd = bigEndianToNative!ubyte(*cast(ubyte[1]*)(data.ptr+pos)); ++pos;
129 		ushort result = bigEndianToNative!ushort(*cast(ubyte[2]*)(data.ptr+pos)); pos+=2;
130 		uint time = bigEndianToNative!uint(*cast(ubyte[4]*)(data.ptr+pos)); pos+=4;
131 		ushort private_port = bigEndianToNative!ushort(*cast(ubyte[2]*)(data.ptr+pos)); pos+=2;
132 		ushort public_port = bigEndianToNative!ushort(*cast(ubyte[2]*)(data.ptr+pos)); pos+=2;
133 		uint lifetime = bigEndianToNative!uint(*cast(ubyte[4]*)(data.ptr+pos));
134 
135 		bool is_tcp = (cmd - 128 == 1)?false:true;
136 
137 		static if (LOG) logInfo("Got version %d, cmd %d, result %d, time %d, private_port %d, public_port %d, lifetime %d, is_tcp %s",
138 			version_, cmd, result, time, private_port, public_port, lifetime, is_tcp.to!string);
139 
140 		// validate
141 		enforce(version_ == 0, "Invalid NAT-PMP version: " ~ version_.to!string);
142 
143 		if (result != 0) {
144 			string[] errors = [
145 				"Unsupported protocol version",
146 				"Not authorized to create port map (enable NAT-PMP on your router)",
147 				"Network failure",
148 				"Out of resources",
149 				"Unsupported opcode"
150 			];
151 			throw new NATPMPException(errors[result-1]);
152 		}
153 	}
154 }
155