├── .gitignore
├── FUNDING.yml
├── LICENCE
├── MLAPI.Puncher.Client.Console
├── MLAPI.Puncher.Client.Console.csproj
└── Program.cs
├── MLAPI.Puncher.Client
├── Exceptions
│ ├── ServerNotReachableException.cs
│ └── SocketSendException.cs
├── MLAPI.Puncher.Client.csproj
└── PuncherClient.cs
├── MLAPI.Puncher.Server.Console
├── MLAPI.Puncher.Server.Console.csproj
└── Program.cs
├── MLAPI.Puncher.Server
├── Client.cs
├── MLAPI.Puncher.Server.csproj
└── PuncherServer.cs
├── MLAPI.Puncher.Shared
├── Constants.cs
├── ErrorType.cs
├── IUDPTransport.cs
├── MLAPI.Puncher.Shared.projitems
├── MLAPI.Puncher.Shared.shproj
├── MessageType.cs
└── SlimUDPTransport.cs
├── MLAPI.Puncher.sln
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # globs
2 | Makefile.in
3 | *.userprefs
4 | *.usertasks
5 | config.make
6 | config.status
7 | aclocal.m4
8 | install-sh
9 | autom4te.cache/
10 | *.tar.gz
11 | tarballs/
12 | test-results/
13 |
14 | # Mac bundle stuff
15 | *.dmg
16 | *.app
17 |
18 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
19 | # General
20 | .DS_Store
21 | .AppleDouble
22 | .LSOverride
23 |
24 | # Icon must end with two \r
25 | Icon
26 |
27 |
28 | # Thumbnails
29 | ._*
30 |
31 | # Files that might appear in the root of a volume
32 | .DocumentRevisions-V100
33 | .fseventsd
34 | .Spotlight-V100
35 | .TemporaryItems
36 | .Trashes
37 | .VolumeIcon.icns
38 | .com.apple.timemachine.donotpresent
39 |
40 | # Directories potentially created on remote AFP share
41 | .AppleDB
42 | .AppleDesktop
43 | Network Trash Folder
44 | Temporary Items
45 | .apdisk
46 |
47 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
48 | # Windows thumbnail cache files
49 | Thumbs.db
50 | ehthumbs.db
51 | ehthumbs_vista.db
52 |
53 | # Dump file
54 | *.stackdump
55 |
56 | # Folder config file
57 | [Dd]esktop.ini
58 |
59 | # Recycle Bin used on file shares
60 | $RECYCLE.BIN/
61 |
62 | # Windows Installer files
63 | *.cab
64 | *.msi
65 | *.msix
66 | *.msm
67 | *.msp
68 |
69 | # Windows shortcuts
70 | *.lnk
71 |
72 | # content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
73 | ## Ignore Visual Studio temporary files, build results, and
74 | ## files generated by popular Visual Studio add-ons.
75 | ##
76 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
77 |
78 | # User-specific files
79 | *.suo
80 | *.user
81 | *.userosscache
82 | *.sln.docstates
83 |
84 | # User-specific files (MonoDevelop/Xamarin Studio)
85 | *.userprefs
86 |
87 | # Build results
88 | [Dd]ebug/
89 | [Dd]ebugPublic/
90 | [Rr]elease/
91 | [Rr]eleases/
92 | x64/
93 | x86/
94 | bld/
95 | [Bb]in/
96 | [Oo]bj/
97 | [Ll]og/
98 |
99 | # Visual Studio 2015/2017 cache/options directory
100 | .vs/
101 | # Uncomment if you have tasks that create the project's static files in wwwroot
102 | #wwwroot/
103 |
104 | # Visual Studio 2017 auto generated files
105 | Generated\ Files/
106 |
107 | # MSTest test Results
108 | [Tt]est[Rr]esult*/
109 | [Bb]uild[Ll]og.*
110 |
111 | # NUNIT
112 | *.VisualState.xml
113 | TestResult.xml
114 |
115 | # Build Results of an ATL Project
116 | [Dd]ebugPS/
117 | [Rr]eleasePS/
118 | dlldata.c
119 |
120 | # Benchmark Results
121 | BenchmarkDotNet.Artifacts/
122 |
123 | # .NET Core
124 | project.lock.json
125 | project.fragment.lock.json
126 | artifacts/
127 |
128 | # StyleCop
129 | StyleCopReport.xml
130 |
131 | # Files built by Visual Studio
132 | *_i.c
133 | *_p.c
134 | *_h.h
135 | *.ilk
136 | *.meta
137 | *.obj
138 | *.iobj
139 | *.pch
140 | *.pdb
141 | *.ipdb
142 | *.pgc
143 | *.pgd
144 | *.rsp
145 | *.sbr
146 | *.tlb
147 | *.tli
148 | *.tlh
149 | *.tmp
150 | *.tmp_proj
151 | *_wpftmp.csproj
152 | *.log
153 | *.vspscc
154 | *.vssscc
155 | .builds
156 | *.pidb
157 | *.svclog
158 | *.scc
159 |
160 | # Chutzpah Test files
161 | _Chutzpah*
162 |
163 | # Visual C++ cache files
164 | ipch/
165 | *.aps
166 | *.ncb
167 | *.opendb
168 | *.opensdf
169 | *.sdf
170 | *.cachefile
171 | *.VC.db
172 | *.VC.VC.opendb
173 |
174 | # Visual Studio profiler
175 | *.psess
176 | *.vsp
177 | *.vspx
178 | *.sap
179 |
180 | # Visual Studio Trace Files
181 | *.e2e
182 |
183 | # TFS 2012 Local Workspace
184 | $tf/
185 |
186 | # Guidance Automation Toolkit
187 | *.gpState
188 |
189 | # ReSharper is a .NET coding add-in
190 | _ReSharper*/
191 | *.[Rr]e[Ss]harper
192 | *.DotSettings.user
193 |
194 | # JustCode is a .NET coding add-in
195 | .JustCode
196 |
197 | # TeamCity is a build add-in
198 | _TeamCity*
199 |
200 | # DotCover is a Code Coverage Tool
201 | *.dotCover
202 |
203 | # AxoCover is a Code Coverage Tool
204 | .axoCover/*
205 | !.axoCover/settings.json
206 |
207 | # Visual Studio code coverage results
208 | *.coverage
209 | *.coveragexml
210 |
211 | # NCrunch
212 | _NCrunch_*
213 | .*crunch*.local.xml
214 | nCrunchTemp_*
215 |
216 | # MightyMoose
217 | *.mm.*
218 | AutoTest.Net/
219 |
220 | # Web workbench (sass)
221 | .sass-cache/
222 |
223 | # Installshield output folder
224 | [Ee]xpress/
225 |
226 | # DocProject is a documentation generator add-in
227 | DocProject/buildhelp/
228 | DocProject/Help/*.HxT
229 | DocProject/Help/*.HxC
230 | DocProject/Help/*.hhc
231 | DocProject/Help/*.hhk
232 | DocProject/Help/*.hhp
233 | DocProject/Help/Html2
234 | DocProject/Help/html
235 |
236 | # Click-Once directory
237 | publish/
238 |
239 | # Publish Web Output
240 | *.[Pp]ublish.xml
241 | *.azurePubxml
242 | # Note: Comment the next line if you want to checkin your web deploy settings,
243 | # but database connection strings (with potential passwords) will be unencrypted
244 | *.pubxml
245 | *.publishproj
246 |
247 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
248 | # checkin your Azure Web App publish settings, but sensitive information contained
249 | # in these scripts will be unencrypted
250 | PublishScripts/
251 |
252 | # NuGet Packages
253 | *.nupkg
254 | # The packages folder can be ignored because of Package Restore
255 | **/[Pp]ackages/*
256 | # except build/, which is used as an MSBuild target.
257 | !**/[Pp]ackages/build/
258 | # Uncomment if necessary however generally it will be regenerated when needed
259 | #!**/[Pp]ackages/repositories.config
260 | # NuGet v3's project.json files produces more ignorable files
261 | *.nuget.props
262 | *.nuget.targets
263 |
264 | # Microsoft Azure Build Output
265 | csx/
266 | *.build.csdef
267 |
268 | # Microsoft Azure Emulator
269 | ecf/
270 | rcf/
271 |
272 | # Windows Store app package directories and files
273 | AppPackages/
274 | BundleArtifacts/
275 | Package.StoreAssociation.xml
276 | _pkginfo.txt
277 | *.appx
278 |
279 | # Visual Studio cache files
280 | # files ending in .cache can be ignored
281 | *.[Cc]ache
282 | # but keep track of directories ending in .cache
283 | !*.[Cc]ache/
284 |
285 | # Others
286 | ClientBin/
287 | ~$*
288 | *~
289 | *.dbmdl
290 | *.dbproj.schemaview
291 | *.jfm
292 | *.pfx
293 | *.publishsettings
294 | orleans.codegen.cs
295 |
296 | # Including strong name files can present a security risk
297 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
298 | #*.snk
299 |
300 | # Since there are multiple workflows, uncomment next line to ignore bower_components
301 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
302 | #bower_components/
303 |
304 | # RIA/Silverlight projects
305 | Generated_Code/
306 |
307 | # Backup & report files from converting an old project file
308 | # to a newer Visual Studio version. Backup files are not needed,
309 | # because we have git ;-)
310 | _UpgradeReport_Files/
311 | Backup*/
312 | UpgradeLog*.XML
313 | UpgradeLog*.htm
314 | ServiceFabricBackup/
315 | *.rptproj.bak
316 |
317 | # SQL Server files
318 | *.mdf
319 | *.ldf
320 | *.ndf
321 |
322 | # Business Intelligence projects
323 | *.rdl.data
324 | *.bim.layout
325 | *.bim_*.settings
326 | *.rptproj.rsuser
327 |
328 | # Microsoft Fakes
329 | FakesAssemblies/
330 |
331 | # GhostDoc plugin setting file
332 | *.GhostDoc.xml
333 |
334 | # Node.js Tools for Visual Studio
335 | .ntvs_analysis.dat
336 | node_modules/
337 |
338 | # Visual Studio 6 build log
339 | *.plg
340 |
341 | # Visual Studio 6 workspace options file
342 | *.opt
343 |
344 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
345 | *.vbw
346 |
347 | # Visual Studio LightSwitch build output
348 | **/*.HTMLClient/GeneratedArtifacts
349 | **/*.DesktopClient/GeneratedArtifacts
350 | **/*.DesktopClient/ModelManifest.xml
351 | **/*.Server/GeneratedArtifacts
352 | **/*.Server/ModelManifest.xml
353 | _Pvt_Extensions
354 |
355 | # Paket dependency manager
356 | .paket/paket.exe
357 | paket-files/
358 |
359 | # FAKE - F# Make
360 | .fake/
361 |
362 | # JetBrains Rider
363 | .idea/
364 | *.sln.iml
365 |
366 | # CodeRush personal settings
367 | .cr/personal
368 |
369 | # Python Tools for Visual Studio (PTVS)
370 | __pycache__/
371 | *.pyc
372 |
373 | # Cake - Uncomment if you are using it
374 | # tools/**
375 | # !tools/packages.config
376 |
377 | # Tabs Studio
378 | *.tss
379 |
380 | # Telerik's JustMock configuration file
381 | *.jmconfig
382 |
383 | # BizTalk build output
384 | *.btp.cs
385 | *.btm.cs
386 | *.odx.cs
387 | *.xsd.cs
388 |
389 | # OpenCover UI analysis results
390 | OpenCover/
391 |
392 | # Azure Stream Analytics local run output
393 | ASALocalRun/
394 |
395 | # MSBuild Binary and Structured Log
396 | *.binlog
397 |
398 | # NVidia Nsight GPU debugger configuration file
399 | *.nvuser
400 |
401 | # MFractors (Xamarin productivity tool) working folder
402 | .mfractor/
403 |
404 | # Local History for Visual Studio
405 | .localhistory/
--------------------------------------------------------------------------------
/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: TwoTenPvP
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Albin Corén
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/MLAPI.Puncher.Client.Console/MLAPI.Puncher.Client.Console.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | netcoreapp2.2
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Client.Console/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace MLAPI.Puncher.Client.Console
7 | {
8 | class Program
9 | {
10 | public const string PUNCHER_SERVER_HOST = "puncher.midlevel.io";
11 | public const int PUNCHER_SERVER_PORT = 6776;
12 |
13 | static void Main(string[] args)
14 | {
15 | Task listenTask = Task.Factory.StartNew(() =>
16 | {
17 | try
18 | {
19 | using (PuncherClient listenPeer = new PuncherClient(PUNCHER_SERVER_HOST, PUNCHER_SERVER_PORT))
20 | {
21 | System.Console.WriteLine("[LISTENER] Listening for single punch on our port 1234...");
22 | IPEndPoint endpoint = listenPeer.ListenForSinglePunch(new IPEndPoint(IPAddress.Any, 1234));
23 | System.Console.WriteLine("[LISTENER] Connector: " + endpoint + " punched through our NAT");
24 | }
25 | }
26 | catch (Exception e)
27 | {
28 | System.Console.WriteLine(e);
29 | }
30 | });
31 |
32 | // Wait a bit to make sure the listener has a chance to register.
33 | Thread.Sleep(1000);
34 |
35 | System.Console.Write("[CONNECTOR] Enter the address of the listener you want to punch: ");
36 | string address = System.Console.ReadLine();
37 |
38 | using (PuncherClient connectPeer = new PuncherClient(PUNCHER_SERVER_HOST, PUNCHER_SERVER_PORT))
39 | {
40 | System.Console.WriteLine("[CONNECTOR] Punching...");
41 |
42 | if (connectPeer.TryPunch(IPAddress.Parse(address), out IPEndPoint connectResult))
43 | {
44 | System.Console.WriteLine("[CONNECTOR] Punched through to peer: " + connectResult);
45 | }
46 | else
47 | {
48 | System.Console.WriteLine("[CONNECTOR] Failed to punch");
49 | }
50 |
51 | // Prevent application from exiting before listener has ended
52 | listenTask.Wait();
53 | }
54 |
55 | // For the plebs
56 | System.Console.Read();
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Client/Exceptions/ServerNotReachableException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace MLAPI.Puncher.Client.Exceptions
4 | {
5 | public class ServerNotReachableException : Exception
6 | {
7 | public ServerNotReachableException()
8 | {
9 | }
10 |
11 | public ServerNotReachableException(string message) : base(message)
12 | {
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Client/Exceptions/SocketSendException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace MLAPI.Puncher.Client.Exceptions
4 | {
5 | public class SocketSendException : Exception
6 | {
7 | public SocketSendException()
8 | {
9 | }
10 |
11 | public SocketSendException(string message) : base(message)
12 | {
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Client/MLAPI.Puncher.Client.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net35;net45;net471;netstandard2.0
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Client/PuncherClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Net.Sockets;
6 | using MLAPI.Puncher.Client.Exceptions;
7 | using MLAPI.Puncher.Shared;
8 |
9 | namespace MLAPI.Puncher.Client
10 | {
11 | ///
12 | /// Delegate used on the listener to inform about a connector that punched our NAT.
13 | ///
14 | public delegate void OnConnectorPunchSuccessfulDelegate(IPEndPoint endpoint);
15 |
16 | ///
17 | /// A puncher client capable of pucnhing and being punched.
18 | ///
19 | public class PuncherClient : IDisposable
20 | {
21 | ///
22 | /// Gets or sets the transport used to communicate with puncher server.
23 | ///
24 | /// The transport used to communcate with puncher server.
25 | public IUDPTransport Transport { get; set; } = new SlimUDPTransport();
26 | ///
27 | /// Gets or sets the amount port predictions to attempt.
28 | ///
29 | /// The amount of port predictions to attempt.
30 | public int PortPredictions { get; set; } = 12;
31 | ///
32 | /// Gets or sets the max punch response wait time in milliseconds.
33 | ///
34 | /// The max punch response wait time in milliseconds.
35 | public int PunchResponseTimeout { get; set; } = 8000;
36 | ///
37 | /// Gets or sets the server register response timeout.
38 | /// If the Puncher server does not respond within this time the connection times out in milliseconds.
39 | ///
40 | /// The server register response timeout in milliseconds.
41 | public int ServerRegisterResponseTimeout { get; set; } = 8000;
42 | ///
43 | /// Gets or sets the interval of register requests sent to the server in milliseconds.
44 | ///
45 | /// The server register interval in milliseconds.
46 | public int ServerRegisterInterval { get; set; } = 60_000;
47 | ///
48 | /// Gets or sets the socket send timeout in milliseconds.
49 | ///
50 | /// The socket send timeout in milliseconds.
51 | public int SocketSendTimeout { get; set; } = 500;
52 | ///
53 | /// Gets or sets the socket receive timeout in milliseconds.
54 | ///
55 | /// The socket receive timeout in milliseconds.
56 | public int SocketReceiveTimeout { get; set; } = 500;
57 | ///
58 | /// Gets or sets a value indicating whether this should drop unknown addresses.
59 | /// That is, ignore addresses we have not explicitly connected to. This setting only affect connectors.
60 | ///
61 | /// true if we should drop unknown addresses; otherwise, false.
62 | public bool DropUnknownAddresses { get; set; } = true;
63 | ///
64 | /// Occurs on the listener when a connector punches our NAT.
65 | ///
66 | public event OnConnectorPunchSuccessfulDelegate OnConnectorPunchSuccessful;
67 |
68 | private readonly IPEndPoint[] _puncherServerEndpoints;
69 | private bool _isRunning = false;
70 |
71 | // Buffers
72 | private readonly byte[] _buffer = new byte[Constants.BUFFER_SIZE];
73 | private readonly byte[] _tokenBuffer = new byte[Constants.TOKEN_BUFFER_SIZE];
74 |
75 | ///
76 | /// Initializes a new instance of the class with a specified server endpoint.
77 | ///
78 | /// Puncher server endpoint.
79 | public PuncherClient(IPEndPoint puncherServerEndpoint)
80 | {
81 | _puncherServerEndpoints = new IPEndPoint[1] { puncherServerEndpoint };
82 | }
83 |
84 | ///
85 | /// Initializes a new instance of the class with a specified server endpoints.
86 | ///
87 | /// Puncher server endpoints.
88 | public PuncherClient(IPEndPoint[] puncherServerEndpoint)
89 | {
90 | _puncherServerEndpoints = puncherServerEndpoint;
91 | }
92 |
93 | ///
94 | /// Initializes a new instance of the class with a specified server host and port.
95 | ///
96 | /// Puncher server host.
97 | /// Puncher server port.
98 | public PuncherClient(string puncherServerHost, ushort puncherServerPort)
99 | {
100 | // Send the DNS query
101 | IPHostEntry hostEntry = Dns.GetHostEntry(puncherServerHost);
102 |
103 | // Sort only IPv4 addresses
104 | _puncherServerEndpoints = hostEntry.AddressList.Where(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => new IPEndPoint(x, puncherServerPort)).ToArray();
105 | }
106 |
107 | ///
108 | /// Listens for incoming punch requests.
109 | ///
110 | /// The endpoint where new players should join.
111 | public void ListenForPunches(IPEndPoint listenEndpoint)
112 | {
113 | // Bind the socket
114 | Transport.Bind(listenEndpoint);
115 |
116 | _isRunning = true;
117 |
118 | RunListenerJob(false, false);
119 |
120 | _isRunning = false;
121 | }
122 |
123 | ///
124 | /// Listens for a single punch and returns when the punch is successful.
125 | ///
126 | /// The address of the connector that punched through our NAT.
127 | /// The endpoint where new players should join.
128 | public IPEndPoint ListenForSinglePunch(IPEndPoint listenEndpoint)
129 | {
130 | // Bind the socket
131 | Transport.Bind(listenEndpoint);
132 |
133 | _isRunning = true;
134 |
135 | IPEndPoint endpoint = RunListenerJob(true, false);
136 |
137 | _isRunning = false;
138 |
139 | return endpoint;
140 | }
141 |
142 | ///
143 | /// Starts punching the requested peer.
144 | ///
145 | /// The remote peer connectable address.
146 | /// The peer connect address.
147 | public bool TryPunch(IPAddress connectAddress, out IPEndPoint punchResult)
148 | {
149 | if (connectAddress.AddressFamily != AddressFamily.InterNetwork)
150 | {
151 | throw new ArgumentException("Only IPv4 addresses can be punched. IPv6 addresses does not have to be punched as they dont use NAT.");
152 | }
153 |
154 | // Bind the socket
155 | Transport.Bind(new IPEndPoint(IPAddress.Any, 0));
156 |
157 | // Set running state
158 | _isRunning = true;
159 |
160 | // Generate random token
161 | byte[] token = new byte[32];
162 | Random rnd = new Random();
163 | rnd.NextBytes(token);
164 |
165 | // Default punch result
166 | punchResult = null;
167 |
168 | // Register with NAT server
169 | SendRegisterRequest(connectAddress, token);
170 |
171 | // Waits for response from the puncher server.
172 | if (TryWaitForConnectorRegisterResponse(token, out IPEndPoint punchEndPoint, false))
173 | {
174 | if (DropUnknownAddresses && !punchEndPoint.Address.Equals(connectAddress))
175 | {
176 | // The address we were asked to punch was not the same as the one we connected to.
177 | // This might mean either a proxy, or a malicious interception.
178 | return false;
179 | }
180 |
181 | // Sends punches
182 | SendPunches(punchEndPoint, new ArraySegment(token, 0, token.Length));
183 |
184 | // Waits for PunchSuccess
185 | if (TryCompleteConnectorPunch(punchEndPoint, token, out punchResult) && punchResult != null)
186 | {
187 | _isRunning = false;
188 |
189 | return true;
190 | }
191 | }
192 |
193 |
194 | _isRunning = false;
195 | return false;
196 | }
197 |
198 | #region CLIENT & SERVER
199 |
200 | // Sends a register and waits for a response.
201 | private void SendRegisterRequest(IPAddress connectAddress, byte[] token)
202 | {
203 | // Prevent info leaks
204 | Array.Clear(_buffer, 0, Constants.BUFFER_SIZE);
205 |
206 | // Write message type
207 | _buffer[0] = (byte)MessageType.Register;
208 |
209 |
210 | // Flag byte (1 = (isConnector true && isListener false))
211 | // (2 = (isConnector false && isListener true))
212 |
213 | bool isConnector = connectAddress != null;
214 |
215 | if (isConnector)
216 | {
217 | // Set flag byte
218 | _buffer[1] = 1;
219 |
220 | // Write target address
221 | byte[] addressBytes = connectAddress.GetAddressBytes();
222 |
223 | // Write IPv4 Address
224 | Buffer.BlockCopy(addressBytes, 0, _buffer, 2, 4);
225 |
226 | // Calculate token lenght. Max is 32
227 | byte tokenLength = (byte)Math.Min(token.Length, 32);
228 |
229 | // Write token length
230 | _buffer[6] = tokenLength;
231 |
232 | // Write the token
233 | Buffer.BlockCopy(token, 0, _buffer, 7, token.Length);
234 | }
235 | else
236 | {
237 | // Set flag byte
238 | _buffer[1] = 2;
239 | }
240 |
241 | for (int i = 0; i < _puncherServerEndpoints.Length; i++)
242 | {
243 | // Send register
244 | int size = Transport.SendTo(_buffer, 0, Constants.BUFFER_SIZE, SocketSendTimeout, _puncherServerEndpoints[i]);
245 |
246 | if (size != Constants.BUFFER_SIZE)
247 | {
248 | throw new SocketSendException("Could not send Register packet on socket");
249 | }
250 | }
251 | }
252 |
253 | // Sends punches and punch predictions
254 | private void SendPunches(IPEndPoint punchEndpoint, ArraySegment token)
255 | {
256 | // Write punch
257 | _buffer[0] = (byte)MessageType.Punch;
258 |
259 | // Write token length
260 | _buffer[1] = (byte)token.Count;
261 |
262 | // Write token
263 | Buffer.BlockCopy(token.Array, token.Offset, _buffer, 2, (byte)token.Count);
264 |
265 | for (int i = 0; i < PortPredictions; i++)
266 | {
267 | // Send all punches
268 | int size = Transport.SendTo(_buffer, 0, Constants.BUFFER_SIZE, SocketSendTimeout, new IPEndPoint(punchEndpoint.Address, punchEndpoint.Port + i));
269 |
270 | if (size != Constants.BUFFER_SIZE)
271 | {
272 | throw new SocketSendException("Could not send Punch packet on socket");
273 | }
274 | }
275 | }
276 |
277 | #endregion
278 |
279 | #region LISTENER
280 |
281 | internal struct ListenerResponseStatus
282 | {
283 | public IPEndPoint EndPoint;
284 | public bool IsWaitingForResponse;
285 | public DateTime LastRegisterTime;
286 | }
287 |
288 | private IPEndPoint RunListenerJob(bool exitOnSuccessfulPunch, bool timeoutException)
289 | {
290 | // Register and punch loop
291 | SendRegisterRequest(null, null);
292 |
293 | // Create listener status array
294 | ListenerResponseStatus[] statuses = new ListenerResponseStatus[_puncherServerEndpoints.Length];
295 | for (int i = 0; i < statuses.Length; i++)
296 | {
297 | // Set defaults
298 | statuses[i] = new ListenerResponseStatus()
299 | {
300 | EndPoint = _puncherServerEndpoints[i],
301 | IsWaitingForResponse = false,
302 | LastRegisterTime = DateTime.Now
303 | };
304 | }
305 |
306 | while (_isRunning)
307 | {
308 | int size = Transport.ReceiveFrom(_buffer, 0, Constants.BUFFER_SIZE, SocketReceiveTimeout, out IPEndPoint remoteEndPoint);
309 |
310 | int indexOfEndPointStatus = -1;
311 |
312 | for (int i = 0; i < statuses.Length; i++)
313 | {
314 | if (statuses[i].EndPoint.Equals(remoteEndPoint))
315 | {
316 | indexOfEndPointStatus = i;
317 | break;
318 | }
319 | }
320 |
321 | if (size == Constants.BUFFER_SIZE)
322 | {
323 | if (_buffer[0] == (byte)MessageType.Registered && remoteEndPoint != null && _puncherServerEndpoints.Contains(remoteEndPoint))
324 | {
325 | // Registered response.
326 |
327 | statuses[indexOfEndPointStatus].IsWaitingForResponse = false;
328 | }
329 | else if (remoteEndPoint != null)
330 | {
331 | // If the message is not a registration confirmation, we try to parse it as a packet for the listener
332 | if (TryParseListenerPacket(remoteEndPoint, out IPEndPoint successEndPoint))
333 | {
334 | if (successEndPoint != null && exitOnSuccessfulPunch)
335 | {
336 | return successEndPoint;
337 | }
338 | else if (successEndPoint != null && OnConnectorPunchSuccessful != null)
339 | {
340 | OnConnectorPunchSuccessful(successEndPoint);
341 | }
342 | }
343 | }
344 | }
345 |
346 | // Resend and timeout loop
347 | for (int i = 0; i < statuses.Length; i++)
348 | {
349 | if (!statuses[i].IsWaitingForResponse && (DateTime.Now - statuses[i].LastRegisterTime).TotalMilliseconds > ServerRegisterInterval)
350 | {
351 | // Sends new registration request
352 | SendRegisterRequest(null, null);
353 |
354 | // Update last register time
355 | statuses[i].LastRegisterTime = DateTime.Now;
356 | statuses[i].IsWaitingForResponse = true;
357 | }
358 |
359 | // No registration response received within timeout
360 | if (statuses[i].IsWaitingForResponse && (DateTime.Now - statuses[i].LastRegisterTime).TotalMilliseconds > ServerRegisterResponseTimeout && timeoutException)
361 | {
362 | // We got no response to our register request.
363 | throw new ServerNotReachableException("The connection to the PuncherServer \"" + _puncherServerEndpoints + "\" timed out.");
364 | }
365 | }
366 | }
367 |
368 |
369 | return null;
370 | }
371 |
372 | // Handles punch messages
373 | private bool TryParseListenerPacket(IPEndPoint remoteEndPoint, out IPEndPoint punchSuccessEndpoint)
374 | {
375 | // Default endpoint
376 | punchSuccessEndpoint = null;
377 |
378 | if (_buffer[0] == (byte)MessageType.ConnectTo && remoteEndPoint != null && _puncherServerEndpoints.Contains(remoteEndPoint))
379 | {
380 | // Read incoming target address, port, token length and token
381 | IPAddress connectToAddress = new IPAddress(new byte[4] { _buffer[1], _buffer[2], _buffer[3], _buffer[4] });
382 | ushort port = (ushort)((ushort)_buffer[5] | (ushort)_buffer[6] << 8);
383 | byte tokenSize = _buffer[7];
384 |
385 | if (tokenSize > Constants.BUFFER_SIZE - 6)
386 | {
387 | // Invalid token size
388 | return false;
389 | }
390 |
391 | // Copy token
392 | Buffer.BlockCopy(_buffer, 8, _tokenBuffer, 0, tokenSize);
393 |
394 | // Clear incoming data
395 | Array.Clear(_buffer, 0, Constants.BUFFER_SIZE);
396 |
397 | // Send punches
398 | SendPunches(new IPEndPoint(connectToAddress, port), new ArraySegment(_tokenBuffer, 0, tokenSize));
399 |
400 | return true;
401 | }
402 | else if (_buffer[0] == (byte)MessageType.Punch && remoteEndPoint != null)
403 | {
404 | // Change message type, leave the body the same (token length and token)
405 | _buffer[0] = (byte)MessageType.PunchSuccess;
406 |
407 | // Send punch success
408 | int size = Transport.SendTo(_buffer, 0, Constants.BUFFER_SIZE, SocketSendTimeout, remoteEndPoint);
409 |
410 | if (size != Constants.BUFFER_SIZE)
411 | {
412 | throw new SocketSendException("Could not send PunchSuccess packet on socket");
413 | }
414 |
415 | // Return the connector address that punched through our NAT.
416 | punchSuccessEndpoint = remoteEndPoint;
417 |
418 | return true;
419 | }
420 |
421 | return false;
422 | }
423 |
424 | #endregion
425 |
426 | #region CONNECTOR
427 |
428 | private bool TryWaitForConnectorRegisterResponse(byte[] token, out IPEndPoint connectToEndpoint, bool timeoutException)
429 | {
430 | DateTime responseWaitTimeStart = DateTime.Now;
431 | connectToEndpoint = null;
432 |
433 | do
434 | {
435 | int size = Transport.ReceiveFrom(_buffer, 0, Constants.BUFFER_SIZE, SocketReceiveTimeout, out IPEndPoint remoteEndPoint);
436 |
437 | if (size == Constants.BUFFER_SIZE && remoteEndPoint != null && _puncherServerEndpoints.Contains(remoteEndPoint))
438 | {
439 | if (_buffer[0] == (byte)MessageType.Error)
440 | {
441 | connectToEndpoint = null;
442 | return false;
443 | }
444 | else if (_buffer[0] == (byte)MessageType.ConnectTo)
445 | {
446 | IPAddress connectToAddress = new IPAddress(new byte[4] { _buffer[1], _buffer[2], _buffer[3], _buffer[4] });
447 | ushort port = (ushort)((ushort)_buffer[5] | (ushort)_buffer[6] << 8);
448 | byte tokenSize = _buffer[7];
449 |
450 | if (tokenSize > Constants.BUFFER_SIZE - 6)
451 | {
452 | // Invalid token size
453 | continue;
454 | }
455 |
456 | // Copy token
457 | Buffer.BlockCopy(_buffer, 8, _tokenBuffer, 0, tokenSize);
458 |
459 | // Validate the request is correct.
460 | bool correct = true;
461 |
462 | for (int i = 0; i < tokenSize; i++)
463 | {
464 | if (_tokenBuffer[i] != token[i])
465 | {
466 | correct = false;
467 | break;
468 | }
469 | }
470 |
471 | if (!correct)
472 | {
473 | // The token was incorrect. Dont return yet.
474 | // Instead go for further iterations to ensure that there are no clogged messages
475 | continue;
476 | }
477 |
478 | connectToEndpoint = new IPEndPoint(connectToAddress, port);
479 | return true;
480 | }
481 | }
482 | } while ((DateTime.Now - responseWaitTimeStart).TotalMilliseconds < ServerRegisterResponseTimeout && _isRunning);
483 |
484 | if (timeoutException)
485 | {
486 | // We got no response to our register request.
487 | throw new ServerNotReachableException("The connection to the PuncherServer \"" + _puncherServerEndpoints + "\" timed out.");
488 | }
489 | else
490 | {
491 | return false;
492 | }
493 | }
494 |
495 | // Waits for a punch response
496 | private bool TryCompleteConnectorPunch(IPEndPoint punchEndpoint, byte[] token, out IPEndPoint punchedEndpoint)
497 | {
498 | punchedEndpoint = null;
499 | DateTime receiveStart = DateTime.Now;
500 |
501 | do
502 | {
503 | // Receive punch success
504 | int size = Transport.ReceiveFrom(_buffer, 0, Constants.BUFFER_SIZE, SocketReceiveTimeout, out IPEndPoint remoteEndPoint);
505 |
506 | // Santy checks
507 | if (size == Constants.BUFFER_SIZE && remoteEndPoint != null && remoteEndPoint.Address.Equals(punchEndpoint.Address))
508 | {
509 | if ((MessageType)_buffer[0] == MessageType.Punch)
510 | {
511 | // We got the listeners punch. If they punch us from a port we have not yet punched. We want to punch their new port.
512 | // This improves symmetric NAT succeess rates.
513 |
514 | // Make sure token size is the same
515 | if (_buffer[1] == (byte)token.Length)
516 | {
517 | bool correct = true;
518 |
519 | for (int x = 0; x < (byte)token.Length; x++)
520 | {
521 | if (_buffer[2 + x] != token[x])
522 | {
523 | correct = false;
524 | break;
525 | }
526 | }
527 |
528 | if (correct)
529 | {
530 | // Token was correct.
531 |
532 | // If the port we got the punch on is a port we have not yet punched. Use the new port to send new punches (Improves symmetric success)
533 | bool hasPingedPort = false;
534 | for (int x = 0; x < PortPredictions; x++)
535 | {
536 | if (punchEndpoint.Port + x == remoteEndPoint.Port)
537 | {
538 | hasPingedPort = true;
539 | break;
540 | }
541 | }
542 |
543 | if (!hasPingedPort)
544 | {
545 | // They got a totally new port that we have not seen before.
546 | // Lets punch it. We dont need to port predict these new punches
547 | int sendSize = Transport.SendTo(_buffer, 0, Constants.BUFFER_SIZE, SocketSendTimeout, new IPEndPoint(punchEndpoint.Address, remoteEndPoint.Port));
548 |
549 | if (sendSize != Constants.BUFFER_SIZE)
550 | {
551 | throw new SocketSendException("Could not send Punch packet on socket");
552 | }
553 | }
554 | }
555 | }
556 | }
557 | else if (((MessageType)_buffer[0]) == MessageType.PunchSuccess)
558 | {
559 | // We got a punch success.
560 |
561 | // Make sure token size is the same
562 | if (_buffer[1] == (byte)token.Length)
563 | {
564 | bool correct = true;
565 |
566 | for (int x = 0; x < (byte)token.Length; x++)
567 | {
568 | if (_buffer[2 + x] != _tokenBuffer[x])
569 | {
570 | correct = false;
571 | break;
572 | }
573 | }
574 |
575 | if (correct)
576 | {
577 | // Success
578 | punchedEndpoint = remoteEndPoint;
579 | return true;
580 | }
581 | }
582 | }
583 | }
584 |
585 | } while ((DateTime.Now - receiveStart).TotalMilliseconds < PunchResponseTimeout && _isRunning);
586 |
587 | // Timeout
588 | return false;
589 | }
590 |
591 | #endregion
592 |
593 | ///
594 | /// Releases all resource used by the object.
595 | ///
596 | /// Call when you are finished using the
597 | /// . The method leaves the
598 | /// in an unusable state. After calling
599 | /// , you must release all references to the
600 | /// so the garbage collector can reclaim the memory that the
601 | /// was occupying.
602 | public void Dispose()
603 | {
604 | _isRunning = false;
605 | Transport.Close();
606 | }
607 |
608 | ///
609 | /// Closes the instance and releases resources.
610 | ///
611 | public void Close()
612 | {
613 | _isRunning = false;
614 | Transport.Close();
615 | }
616 | }
617 | }
618 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Server.Console/MLAPI.Puncher.Server.Console.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | netcoreapp2.2
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Server.Console/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace MLAPI.Puncher.Server.Console
4 | {
5 | class Program
6 | {
7 | static void Main(string[] args)
8 | {
9 | PuncherServer server = new PuncherServer();
10 | server.Start(new IPEndPoint(IPAddress.Any, 6776));
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Server/Client.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 |
4 | namespace MLAPI.Puncher.Server
5 | {
6 | internal class Client
7 | {
8 | public IPEndPoint EndPoint { get; set; }
9 | public bool IsConnector { get; set; }
10 | public bool IsListener { get; set; }
11 | public DateTime LastRegisterTime { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Server/MLAPI.Puncher.Server.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net35;net45;net471;netstandard2.0
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Server/PuncherServer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net;
4 | using MLAPI.Puncher.Shared;
5 | using System.Threading;
6 |
7 | namespace MLAPI.Puncher.Server
8 | {
9 | ///
10 | /// A puncher server capable of routing and organizing client punches.
11 | ///
12 | public class PuncherServer
13 | {
14 | private readonly byte[] _buffer = new byte[Constants.BUFFER_SIZE];
15 | private readonly byte[] _tokenBuffer = new byte[Constants.TOKEN_BUFFER_SIZE];
16 | private readonly byte[] _ipBuffer = new byte[4];
17 |
18 | private readonly ReaderWriterLockSlim _listenerClientsLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
19 | private readonly Dictionary _listenerClients = new Dictionary();
20 | private Thread _cleanupThread;
21 |
22 | ///
23 | /// Gets or sets the transport used to communicate with puncher clients.
24 | ///
25 | /// The transport used to communcate with puncher clients.
26 | public IUDPTransport Transport { get; set; } = new SlimUDPTransport();
27 |
28 | ///
29 | /// Start a server bound to the specified endpoint.
30 | ///
31 | /// Endpoint.
32 | public void Start(IPEndPoint endpoint)
33 | {
34 | Transport.Bind(endpoint);
35 |
36 | _cleanupThread = new Thread(() =>
37 | {
38 | while (true)
39 | {
40 | _listenerClientsLock.EnterUpgradeableReadLock();
41 |
42 | try
43 | {
44 | List addressesToRemove = new List();
45 |
46 | foreach (Client client in _listenerClients.Values)
47 | {
48 | // Make them expire after 120 seconds
49 | if ((DateTime.Now - client.LastRegisterTime).TotalSeconds > 120)
50 | {
51 | addressesToRemove.Add(client.EndPoint.Address);
52 | }
53 | }
54 |
55 |
56 | if (addressesToRemove.Count > 0)
57 | {
58 | _listenerClientsLock.EnterWriteLock();
59 |
60 | try
61 | {
62 | for (int i = 0; i < addressesToRemove.Count; i++)
63 | {
64 | _listenerClients.Remove(addressesToRemove[i]);
65 | }
66 | }
67 | finally
68 | {
69 | _listenerClientsLock.ExitWriteLock();
70 | }
71 | }
72 |
73 | }
74 | finally
75 | {
76 | _listenerClientsLock.ExitUpgradeableReadLock();
77 | }
78 |
79 | // No point in cleaning more than once every 30 seconds
80 | Thread.Sleep(30_000);
81 | }
82 | })
83 | {
84 | IsBackground = true
85 | };
86 |
87 | _cleanupThread.Start();
88 |
89 | while (true)
90 | {
91 | ProcessMessage();
92 | }
93 | }
94 |
95 | private void ProcessMessage()
96 | {
97 | int receiveSize = Transport.ReceiveFrom(_buffer, 0, _buffer.Length, -1, out IPEndPoint senderEndpoint);
98 |
99 | // Address
100 | IPAddress senderAddress = senderEndpoint.Address;
101 |
102 | if (receiveSize != _buffer.Length)
103 | {
104 | return;
105 | }
106 |
107 | if (_buffer[0] != (byte)MessageType.Register)
108 | {
109 | return;
110 | }
111 |
112 | // Register client packet
113 | byte registerFlags = _buffer[1];
114 | bool isConnector = (registerFlags & 1) == 1;
115 | bool isListener = ((registerFlags >> 1) & 1) == 1;
116 |
117 | if (isListener)
118 | {
119 | _listenerClientsLock.EnterUpgradeableReadLock();
120 |
121 | try
122 | {
123 | if (_listenerClients.TryGetValue(senderAddress, out Client client))
124 | {
125 | _listenerClientsLock.EnterWriteLock();
126 |
127 | try
128 | {
129 | client.EndPoint = senderEndpoint;
130 | client.IsConnector = isConnector;
131 | client.IsListener = isListener;
132 | client.LastRegisterTime = DateTime.Now;
133 | }
134 | finally
135 | {
136 | _listenerClientsLock.ExitWriteLock();
137 | }
138 | }
139 | else
140 | {
141 | _listenerClientsLock.EnterWriteLock();
142 |
143 | try
144 | {
145 | _listenerClients.Add(senderAddress, new Client()
146 | {
147 | EndPoint = senderEndpoint,
148 | IsConnector = isConnector,
149 | IsListener = isListener,
150 | LastRegisterTime = DateTime.Now
151 | });
152 | }
153 | finally
154 | {
155 | _listenerClientsLock.ExitWriteLock();
156 | }
157 | }
158 | }
159 | finally
160 | {
161 | _listenerClientsLock.ExitUpgradeableReadLock();
162 | }
163 |
164 | // Prevent info leaks
165 | Array.Clear(_buffer, 0, _buffer.Length);
166 |
167 | // Write message type
168 | _buffer[0] = (byte)MessageType.Registered;
169 |
170 | // Send to listener
171 | Transport.SendTo(_buffer, 0, _buffer.Length, -1, senderEndpoint);
172 | }
173 |
174 | if (isConnector)
175 | {
176 | // Copy address to address buffer
177 | Buffer.BlockCopy(_buffer, 2, _ipBuffer, 0, 4);
178 |
179 | // Parse address
180 | IPAddress listenerAddress = new IPAddress(_ipBuffer);
181 |
182 | // Read token size
183 | byte tokenSize = _buffer[6];
184 |
185 | // Validate token size
186 | if (tokenSize > _buffer.Length - 6)
187 | {
188 | // Invalid token size
189 | return;
190 | }
191 |
192 | // Copy token to token buffer
193 | Buffer.BlockCopy(_buffer, 7, _tokenBuffer, 0, tokenSize);
194 |
195 | _listenerClientsLock.EnterReadLock();
196 |
197 | try
198 | {
199 | // Look for the client they wish to connec tto
200 | if (_listenerClients.TryGetValue(listenerAddress, out Client listenerClient) && listenerClient.IsListener)
201 | {
202 | // Write message type
203 | _buffer[0] = (byte)MessageType.ConnectTo;
204 |
205 | // Write address
206 | Buffer.BlockCopy(listenerClient.EndPoint.Address.GetAddressBytes(), 0, _buffer, 1, 4);
207 |
208 | // Write port
209 | _buffer[5] = (byte)listenerClient.EndPoint.Port;
210 | _buffer[6] = (byte)(listenerClient.EndPoint.Port >> 8);
211 |
212 | // Write token length
213 | _buffer[7] = tokenSize;
214 |
215 | // Write token
216 | Buffer.BlockCopy(_tokenBuffer, 0, _buffer, 8, tokenSize);
217 |
218 | // Send to connector
219 | Transport.SendTo(_buffer, 0, _buffer.Length, -1, senderEndpoint);
220 |
221 | // Write address
222 | Buffer.BlockCopy(senderAddress.GetAddressBytes(), 0, _buffer, 1, 4);
223 |
224 | // Write port
225 | _buffer[5] = (byte)senderEndpoint.Port;
226 | _buffer[6] = (byte)(senderEndpoint.Port >> 8);
227 |
228 | // Send to listener
229 | Transport.SendTo(_buffer, 0, _buffer.Length, -1, listenerClient.EndPoint);
230 | }
231 | else
232 | {
233 | // Prevent info leaks
234 | Array.Clear(_buffer, 0, _buffer.Length);
235 |
236 | _buffer[0] = (byte)MessageType.Error;
237 | _buffer[1] = (byte)ErrorType.ClientNotFound;
238 |
239 | // Send error
240 | Transport.SendTo(_buffer, 0, _buffer.Length, -1, senderEndpoint);
241 | }
242 | }
243 | finally
244 | {
245 | _listenerClientsLock.ExitReadLock();
246 | }
247 | }
248 | }
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Shared/Constants.cs:
--------------------------------------------------------------------------------
1 | namespace MLAPI.Puncher.Shared
2 | {
3 | public static class Constants
4 | {
5 | public static readonly int BUFFER_SIZE = 64;
6 | public static readonly int TOKEN_BUFFER_SIZE = 64;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Shared/ErrorType.cs:
--------------------------------------------------------------------------------
1 | namespace MLAPI.Puncher.Shared
2 | {
3 | ///
4 | /// Represents a error with a punch request.
5 | ///
6 | public enum ErrorType
7 | {
8 | ///
9 | /// The client was not found on the server.
10 | ///
11 | ClientNotFound
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Shared/IUDPTransport.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 |
3 | namespace MLAPI.Puncher.Shared
4 | {
5 | ///
6 | /// Represents the transport protocol where the puncher communicates.
7 | ///
8 | public interface IUDPTransport
9 | {
10 | ///
11 | /// Sends bytes to endpoint.
12 | ///
13 | /// The bytes sent. 0 or less if failed.
14 | /// The buffer to send.
15 | /// The offset of the buffer to start sending at.
16 | /// The length to send from the buffer.
17 | /// The operation timeout in milliseconds.
18 | /// The endpoint to send to.
19 | int SendTo(byte[] buffer, int offset, int length, int timeoutMs, IPEndPoint endpoint);
20 | ///
21 | /// Receives bytes from endpoint.
22 | ///
23 | /// The amount of bytes received. 0 or elss if failed.
24 | /// The buffer to receive to.
25 | /// The offer of the buffer to receive at.
26 | /// The max length to receive.
27 | /// The operation timeout in milliseconds.
28 | /// The endpoint the packet came from.
29 | int ReceiveFrom(byte[] buffer, int offset, int length, int timeoutMs, out IPEndPoint endpoint);
30 | ///
31 | /// Bind transport the specified local endpoint.
32 | ///
33 | /// The local endpoint to bind to.
34 | void Bind(IPEndPoint endpoint);
35 | ///
36 | /// Closes the transport.
37 | ///
38 | void Close();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Shared/MLAPI.Puncher.Shared.projitems:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
5 | true
6 | {6C350A70-FD05-4CD5-8B0F-4568D535119B}
7 |
8 |
9 | MLAPI.Puncher.Shared
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Shared/MLAPI.Puncher.Shared.shproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {6C350A70-FD05-4CD5-8B0F-4568D535119B}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Shared/MessageType.cs:
--------------------------------------------------------------------------------
1 | namespace MLAPI.Puncher.Shared
2 | {
3 | ///
4 | /// Represents the different network message types.
5 | ///
6 | public enum MessageType
7 | {
8 | ///
9 | /// Sent by client to register a listen or a connection request.
10 | ///
11 | Register,
12 | Registered,
13 | ///
14 | /// Sent by server to inform the listening and connecting client to knock on each others NAT.
15 | ///
16 | ConnectTo,
17 | ///
18 | /// Sent by server to explain errors
19 | ///
20 | Error,
21 | ///
22 | /// Sent by listener and connecting client to knock on the other clients NAT.
23 | ///
24 | Punch,
25 | ///
26 | /// Sent by listener client to inform the connecting client that the connecting clients messages got through his NAT.
27 | ///
28 | PunchSuccess
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.Shared/SlimUDPTransport.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Sockets;
3 |
4 | namespace MLAPI.Puncher.Shared
5 | {
6 | ///
7 | /// Default UDP transport implementation
8 | ///
9 | public class SlimUDPTransport : IUDPTransport
10 | {
11 | private readonly Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
12 |
13 | ///
14 | /// Binds the UDP socket to the specified local endpoint.
15 | ///
16 | /// The local endpoint to bind to.
17 | public void Bind(IPEndPoint endpoint)
18 | {
19 | // Allow socket to be reused
20 | _socket.ExclusiveAddressUse = false;
21 | _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
22 |
23 | // Bind the socket
24 | _socket.Bind(endpoint);
25 | }
26 |
27 | ///
28 | /// Closes the UDP socket.
29 | ///
30 | public void Close()
31 | {
32 | // Close the socket
33 | _socket.Close();
34 | }
35 |
36 | ///
37 | /// Receives bytes from endpoint.
38 | ///
39 | /// The amount of bytes received. 0 or elss if failed.
40 | /// The buffer to receive to.
41 | /// The offer of the buffer to receive at.
42 | /// The max length to receive.
43 | /// The operation timeout in milliseconds.
44 | /// The endpoint the packet came from.
45 | public int ReceiveFrom(byte[] buffer, int offset, int length, int timeoutMs, out IPEndPoint endpoint)
46 | {
47 | _socket.ReceiveTimeout = timeoutMs;
48 |
49 | try
50 | {
51 | EndPoint inEndpoint = new IPEndPoint(IPAddress.Any, 0);
52 | int size = _socket.ReceiveFrom(buffer, offset, length, SocketFlags.None, ref inEndpoint);
53 | endpoint = (IPEndPoint)inEndpoint;
54 |
55 | return size;
56 | }
57 | catch (SocketException e)
58 | {
59 | if (e.SocketErrorCode == SocketError.TimedOut)
60 | {
61 | endpoint = null;
62 | return -1;
63 | }
64 | else
65 | {
66 | throw e;
67 | }
68 | }
69 | }
70 |
71 | ///
72 | /// Sends bytes to endpoint.
73 | ///
74 | /// The bytes sent. 0 or less if failed.
75 | /// The buffer to send.
76 | /// The offset of the buffer to start sending at.
77 | /// The length to send from the buffer.
78 | /// The operation timeout in milliseconds.
79 | /// The endpoint to send to.
80 | public int SendTo(byte[] buffer, int offset, int length, int timeoutMs, IPEndPoint endpoint)
81 | {
82 | _socket.SendTimeout = timeoutMs;
83 |
84 | try
85 | {
86 | // X marks the spot (See note below)
87 |
88 | return _socket.SendTo(buffer, offset, length, SocketFlags.None, endpoint);
89 | }
90 | catch (SocketException e)
91 | {
92 | if (e.SocketErrorCode == SocketError.TimedOut)
93 | {
94 | return -1;
95 | }
96 | else
97 | {
98 | /*
99 | * YES, this is litterarly as stupid as it looks. On Linux I consistently get an exception thrown here
100 | * UnknownError with the dotnet runtime (Distro Arch, netcoreapp2.2, SDK 2.2.105)
101 | * AccessDenied on Mono (Distro Arch, JIT 5.20.1)
102 | * HOWEVER! on the second send, it works. Thus we only catch the first one and if the error occurs again we throw it.
103 | * What makes this bug basically IMPOSSIBLE to debug is that if you enter Console.WriteLine(endpoint); on the X marks the spot comment above the code will work...
104 | * Yes... You just print the endpoint. It happens with 100% consitency on both Mono and dotnet. This is super dirty but it works...
105 | * Its not a race either because Sleeping does not help. I am really clueless. Also, timeouts does not affect it.
106 | */
107 | try
108 | {
109 | System.Console.WriteLine(endpoint);
110 | return _socket.SendTo(buffer, offset, length, SocketFlags.None, endpoint);
111 | }
112 | catch (SocketException ex)
113 | {
114 | if (ex.SocketErrorCode == SocketError.TimedOut)
115 | {
116 | return -1;
117 | }
118 | else
119 | {
120 | throw ex;
121 | }
122 | }
123 | }
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/MLAPI.Puncher.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLAPI.Puncher.Client", "MLAPI.Puncher.Client\MLAPI.Puncher.Client.csproj", "{22B86718-BC15-4697-803E-742B9789C07C}"
5 | EndProject
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLAPI.Puncher.Server", "MLAPI.Puncher.Server\MLAPI.Puncher.Server.csproj", "{DB2496DA-F3D2-468F-8187-A7A3F9907097}"
7 | EndProject
8 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MLAPI.Puncher.Shared", "MLAPI.Puncher.Shared\MLAPI.Puncher.Shared.shproj", "{6C350A70-FD05-4CD5-8B0F-4568D535119B}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLAPI.Puncher.Server.Console", "MLAPI.Puncher.Server.Console\MLAPI.Puncher.Server.Console.csproj", "{015D6034-4462-4C17-A609-C26E31DEF432}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MLAPI.Puncher.Client.Console", "MLAPI.Puncher.Client.Console\MLAPI.Puncher.Client.Console.csproj", "{E138A2D3-F844-4609-82E6-C512F09F73B3}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {22B86718-BC15-4697-803E-742B9789C07C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {22B86718-BC15-4697-803E-742B9789C07C}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {22B86718-BC15-4697-803E-742B9789C07C}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {22B86718-BC15-4697-803E-742B9789C07C}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {DB2496DA-F3D2-468F-8187-A7A3F9907097}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {DB2496DA-F3D2-468F-8187-A7A3F9907097}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {DB2496DA-F3D2-468F-8187-A7A3F9907097}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {DB2496DA-F3D2-468F-8187-A7A3F9907097}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {015D6034-4462-4C17-A609-C26E31DEF432}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {015D6034-4462-4C17-A609-C26E31DEF432}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {015D6034-4462-4C17-A609-C26E31DEF432}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {015D6034-4462-4C17-A609-C26E31DEF432}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {E138A2D3-F844-4609-82E6-C512F09F73B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {E138A2D3-F844-4609-82E6-C512F09F73B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {E138A2D3-F844-4609-82E6-C512F09F73B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {E138A2D3-F844-4609-82E6-C512F09F73B3}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MLAPI.Puncher
2 | MLAPI.Puncher is a lightweight, cross-platform, easy to use, tiny implementation (<500 lines) of NAT punchthrough. It has both a server and client.
3 |
4 | ## Features
5 | * Supports Full Cone
6 | * Supports Address-Restricted Cone
7 | * Supports Port-Restricted Cone
8 | * Supports Symmetric Cone (with port prediction if both parties have symmetric cones. Requires sequential port assignment)
9 | * Server and Client implemented in <500 lines of code
10 | * Dependency free
11 | * Transport independent (can integrate into other transports to share a port without interfering)
12 | * Runs on NET 3.5 and above (currently targets ``net35;net45;net471;netstandard2.0``)
13 | * Tested on .NET Core, Mono and .NET Framework on Windows and Linux. Should work everywhere with socket access
14 | * Listener allows multiple people to punch at once
15 | * Token based to detect errors and missed punches
16 | * Fast, Punches in 10-20 ms (on localhost, latency not included)
17 | * Well commented code (read the flow below to get more understanding)
18 | * Safe against routing attacks by validating addresses (optional)
19 | * Multi server cluster supported (You can run multiple puncher servers, and clients will use all of them)
20 |
21 | ## Usage
22 |
23 | ### Server
24 | To start a server, use the MLAPI.Puncher.Server library. This library can run anywhere, for example as a console application (example in MLAPI.Puncher.Server.Console).
25 | To start the server, simply use:
26 |
27 | ```csharp
28 | PuncherServer server = new PuncherServer();
29 | // 6776 is the port of the NAT server. Can be changed.
30 | server.Start(new IPEndPoint(IPAddress.Any, 6776));
31 | ```
32 |
33 | ### Client
34 | To use the client, you need the MLAPI.Puncher.Client library. This is what you use in your applications. An example of a console application can be found in MLAPI.Puncher.Client.Console.
35 |
36 | #### Listener
37 | The client has two parts, one part that is used by anyone who wants to allow other people to connect to them. This can be done like this:
38 |
39 | ```csharp
40 | // Creates the listener with the address and port of the server.
41 | // Disposal stops everything and closes the connection.
42 | using (PuncherClient listener = new PuncherClient("puncher.midlevel.io", 6776))
43 | {
44 | // 1234 is the port where the other peer will connect and punch through.
45 | // That would be the port where your program is going to be listening after the punch is done.
46 | listener.ListenForPunches(new IPEndPoint(IPAddress.Any, 1234));
47 | }
48 | ```
49 |
50 | Note that this will not return as it will continue to listen for punches. Its recommended to be ran in a thread. (If you want a method that exits, you can use ``listener.ListenForSinglePunch`` which gives the EndPoint of the connector)
51 |
52 | #### Connector
53 | The second part is the connector. The party that wants to connect to a listener. It can be started with:
54 |
55 | ```csharp
56 | // Get listener public IP address by means of a matchmaker or otherwise.
57 | string listenerAddress = "46.51.179.90"
58 |
59 | // Creates the connector with the address and port to the server.
60 | // Disposal stops everything and closes the connection.
61 | using (PuncherClient connector = new PuncherClient("puncher.midlevel.io", 6776))
62 | {
63 | // Punches and returns the result
64 | if (connector.TryPunch(IPAddress.Parse(listenerAddress), out IPEndPoint remoteEndPoint);
65 | {
66 | // NAT Punchthrough was successful. It can now be connected to using your normal connection logic.
67 | Connect(remoteEndpoint);
68 | }
69 | else
70 | {
71 | // NAT Punchthrough failed.
72 | }
73 | }
74 | ```
75 |
76 | If the connector is successful in punching through, the remote address and the port that was punched through will be provided in the out endpoint from the StartConnect method. If it failed, it will return false and the endpoint will be null.
77 |
78 | ## Settings
79 | The PuncherClient has a few settings that can be tweaked. They are listed below.
80 |
81 |
82 | #### Transport
83 | This is the transport you want to use, a transport is a class that inherits IUDPTransport and it's what handles all socket access. This allows you to integrate this into any networking library that has some sort of "Unconnected messages" functionality. It defaults to ``new SlimUDPTransport()`` which is just a wrapper around the Socket class.
84 |
85 | #### PortPredictions
86 | Port predictions are the amount of ports to be predicted, this is useful for solving symmetric NAT configurations that assigns ports in sequential order. What it actually does is, if you punch at port X and port prediction is set to 2. It will punch at X, X+1 and X+2.
87 |
88 | #### PunchResponseTimeout
89 | This is the time the Connector will wait for a punch response before assuming the punchthrough failed.
90 |
91 | #### ServerRegisterResponseTimeout
92 | This is the timeout for connecting to the Puncher server. If no response is received within this time, an exception will be thrown.
93 |
94 | #### ServerRegisterInterval
95 | This is the interval at which a listener will resend Register requests. This is because servers will clear their records if a client is not sending pings regularly. Default is 60 seconds, default server clear interval is 120 seconds.
96 |
97 | #### SocketSendTimeout
98 | This is the timeout sent to the Transport Send methods. The default value is 500 milliseconds.
99 |
100 | #### SocketReceiveTimeout
101 | This is the timeout sent to the Transport Receive methods. You want to keep this fairly low as all timeouts depend on the receive methods returning rather quickly. The default value is 500 milliseconds.
102 |
103 | #### DropUnknownAddresses
104 | If a connector sends a connect request to the Punch server, and gets a response that has a different address than the one we requested, and this option is turned on, the punch will be ignored. This could mean either that a proxy is used or that a routing attac is being performed. This defaults to true and is recommended to be set to true if you dont trust the punch server.
105 |
106 | ## Examples
107 | Both client and server has example projects. See MLAPI.Puncher.Server.Console and MLAPI.Puncher.Client.Console.
108 |
109 | ## Public punch server
110 | Currently, you can use the public punch server ``puncher.midlevel.io`` on port ``6776``.
111 |
112 | **This server has NO guaranteed uptime and is not recommended for production. Feel free to use it for testing**.
113 |
114 | ## Future Improvements
115 | * Optimize socket code on the server, dont use single threaded blocking sockets. Minimal data has to be shared across threads anyways (only the listening clients lookup table)
116 | * ~~Error handling, detect what went wrong~~
117 | * Detect cone type
118 | * ~~Improve the amount of simultaneous connectors that a listener can knock at a time~~
119 | * Cryptographically secure (probably not going to be done, just implementing certificates requires reliability + some fragmentation because certificates are really large). Also, there is not too much value in incercepting this, all addresss are verified anyways to be correct. It's just the port that is resolved.
120 |
121 | ## Flow
122 | Definitions:
123 | ```
124 | LC = Listener Client
125 | CC = Connector Client
126 | PS = Puncher Server
127 | Address = IPv4 Address WITHOUT Port
128 | EndPoint = IPv4 Address AND Associated Port
129 | ```
130 |
131 | 1. LC sends Register packet to PS to inform that it's now ready to listen.
132 | 2. PS adds the listeners address to a lookup table. The key is the address (excluding port) and the value is is the endpoint (with port).
133 | 3. PS sends Registered packet to confirm registration to LC. If LC does not receive the Registered packet, it times out.
134 | 4. CC sends Register packet to PS. Included in the packet is the address of the listener he wishes to connect to.
135 | 5. PS looks up if the address is found. If it is not, it sends a Error packet back to CC. If it is found, it sends a ConnectTo packet to CC with LC's endpoint, and a ConnectTo packet to LC with CC's endpoint.
136 | 6. * LC receives ConnectTo packet and sends (PuncherClient.PortPredictions) amount of Punch packets to CC. If the ConnectTo packet contains the address 10.10.10.10 and the port 2785, and PuncherClient.PortPredictions is set to 5. It sends 5 Punch packets, one on each of the following ports: 2785, 2786, 2787, 2788. (This is to try to trick symmetric NATs with sequential assignment)
137 | * CC receives ConnectTo packet and does the same as LC does in step 6.1, but sends them to LC instead of CC.
138 | 7. * If CC gets a punch request from a port that it has not yet punched on. It will send a new punch on that port.
139 | * If LC gets a punch request, it responds with a PunchSuccess packet to the sender of that punch request. (Method will exit if using ListenForSinglePunch)
140 | * If CC gets a PunchSuccess packet, it will return the **endpoint** of where the PunchSuccess packet came from.
141 |
--------------------------------------------------------------------------------