├── .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 | --------------------------------------------------------------------------------