diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6fdaafddc..009430f92 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -18,12 +18,13 @@
     <PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
     <PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
     <PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" />
-    <PackageVersion Include="LibHac" Version="0.18.0" />
+    <PackageVersion Include="LibHac" Version="0.19.0" />
     <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
     <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
     <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
     <PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
     <PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
+    <PackageVersion Include="NetCoreServer" Version="7.0.0" />
     <PackageVersion Include="NUnit" Version="3.13.3" />
     <PackageVersion Include="NUnit3TestAdapter" Version="4.1.0" />
     <PackageVersion Include="OpenTK.Core" Version="4.7.7" />
diff --git a/README.md b/README.md
index 56333278f..b2a6646f5 100644
--- a/README.md
+++ b/README.md
@@ -141,4 +141,5 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY
 
 - [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
 - [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
+- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
 - [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.
diff --git a/distribution/legal/THIRDPARTY.md b/distribution/legal/THIRDPARTY.md
index b0bd5a690..5caa03771 100644
--- a/distribution/legal/THIRDPARTY.md
+++ b/distribution/legal/THIRDPARTY.md
@@ -710,4 +710,4 @@
   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   SOFTWARE.
   ```
-</details>
\ No newline at end of file
+</details>
diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs
index c473cf562..cd066efba 100644
--- a/src/Ryujinx.Ava/AppHost.cs
+++ b/src/Ryujinx.Ava/AppHost.cs
@@ -190,6 +190,7 @@ namespace Ryujinx.Ava
             ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
             ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
 
+            ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
             ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
             ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
 
@@ -408,6 +409,11 @@ namespace Ryujinx.Ava
             });
         }
 
+        private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs<bool> e)
+        {
+            Device.Configuration.EnableInternetAccess = e.NewValue;
+        }
+
         private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs<string> e)
         {
             Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json
index a67b796bd..62aac1227 100644
--- a/src/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -650,7 +650,7 @@
   "UserEditorTitle": "Edit User",
   "UserEditorTitleCreate": "Create User",
   "SettingsTabNetworkInterface": "Network Interface:",
-  "NetworkInterfaceTooltip": "The network interface used for LAN features",
+  "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features",
   "NetworkInterfaceDefault": "Default",
   "PackagingShaders": "Packaging Shaders",
   "AboutChangelogButton": "View Changelog on GitHub",
diff --git a/src/Ryujinx.Ava/Common/ApplicationHelper.cs b/src/Ryujinx.Ava/Common/ApplicationHelper.cs
index b8cd06f3d..91ca8f4d5 100644
--- a/src/Ryujinx.Ava/Common/ApplicationHelper.cs
+++ b/src/Ryujinx.Ava/Common/ApplicationHelper.cs
@@ -173,7 +173,7 @@ namespace Ryujinx.Ava.Common
                 string extension = Path.GetExtension(titleFilePath).ToLower();
                 if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
                 {
-                    PartitionFileSystem pfs;
+                    IFileSystem pfs;
 
                     if (extension == ".xci")
                     {
@@ -181,7 +181,9 @@ namespace Ryujinx.Ava.Common
                     }
                     else
                     {
-                        pfs = new PartitionFileSystem(file.AsStorage());
+                        var pfsTemp = new PartitionFileSystem();
+                        pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+                        pfs = pfsTemp;
                     }
 
                     foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
diff --git a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
index b88bd3d9c..cdecae77d 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/DownloadableContentManagerViewModel.cs
@@ -126,7 +126,8 @@ namespace Ryujinx.Ava.UI.ViewModels
                 {
                     using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
 
-                    PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
+                    PartitionFileSystem partitionFileSystem = new();
+                    partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
 
                     _virtualFileSystem.ImportTickets(partitionFileSystem);
 
@@ -232,7 +233,8 @@ namespace Ryujinx.Ava.UI.ViewModels
 
             using FileStream containerFile = File.OpenRead(path);
 
-            PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
+            PartitionFileSystem partitionFileSystem = new();
+            partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
             bool containsDownloadableContent = false;
 
             _virtualFileSystem.ImportTickets(partitionFileSystem);
diff --git a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
index dd0b92a51..5090a8c70 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/TitleUpdateViewModel.cs
@@ -170,7 +170,9 @@ namespace Ryujinx.Ava.UI.ViewModels
 
                 try
                 {
-                    (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, new PartitionFileSystem(file.AsStorage()), TitleId.ToString("x16"), 0);
+                    var pfs = new PartitionFileSystem();
+                    pfs.Initialize(file.AsStorage()).ThrowIfFailure();
+                    (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, pfs, TitleId.ToString("x16"), 0);
 
                     if (controlNca != null && patchNca != null)
                     {
diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
index 167429433..05108716d 100644
--- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
+++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
@@ -3,5 +3,6 @@
     public enum MultiplayerMode
     {
         Disabled,
+        LdnMitm,
     }
 }
diff --git a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs b/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs
index 98a0d8abf..a968ad17b 100644
--- a/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs
+++ b/src/Ryujinx.Common/SystemInfo/MacOSSystemInfo.cs
@@ -12,6 +12,13 @@ namespace Ryujinx.Common.SystemInfo
     {
         internal MacOSSystemInfo()
         {
+            if (SysctlByName("kern.osversion", out string buildRevision) != 0)
+            {
+                buildRevision = "Unknown Build";
+            }
+
+            OsDescription = $"macOS {Environment.OSVersion.Version} ({buildRevision}) ({RuntimeInformation.OSArchitecture})";
+
             string cpuName = GetCpuidCpuName();
 
             if (cpuName == null && SysctlByName("machdep.cpu.brand_string", out cpuName) != 0)
diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
index 78fb342b1..3b64a28f5 100644
--- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
+++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
@@ -74,5 +74,10 @@ namespace Ryujinx.Common.Utilities
         {
             return ConvertIpv4Address(IPAddress.Parse(ipAddress));
         }
+
+        public static IPAddress ConvertUint(uint ipAddress)
+        {
+            return new IPAddress(new byte[] { (byte)((ipAddress >> 24) & 0xFF), (byte)((ipAddress >> 16) & 0xFF), (byte)((ipAddress >> 8) & 0xFF), (byte)(ipAddress & 0xFF) });
+        }
     }
 }
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
index 5a231079a..55f7d5778 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
@@ -766,7 +766,10 @@ namespace Ryujinx.Graphics.Shader.Instructions
                 flags |= offset == TexOffset.Ptp ? TextureFlags.Offsets : TextureFlags.Offset;
             }
 
-            sourcesList.Add(Const((int)component));
+            if (!hasDepthCompare)
+            {
+                sourcesList.Add(Const((int)component));
+            }
 
             Operand[] sources = sourcesList.ToArray();
             Operand[] dests = new Operand[BitOperations.PopCount((uint)componentMask)];
diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
index 646808e78..724cb675c 100644
--- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs
+++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
@@ -198,7 +198,7 @@ namespace Ryujinx.HLE.FileSystem
             {
                 using var ncaFile = new UniqueRef<IFile>();
 
-                fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read);
+                fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
                 var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
                 if (nca.Header.ContentType != NcaContentType.Meta)
                 {
@@ -210,7 +210,7 @@ namespace Ryujinx.HLE.FileSystem
                 using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel);
                 using var cnmtFile = new UniqueRef<IFile>();
 
-                pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read);
+                pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
 
                 var cnmt = new Cnmt(cnmtFile.Get.AsStream());
                 if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId)
@@ -220,7 +220,7 @@ namespace Ryujinx.HLE.FileSystem
 
                 string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower();
 
-                AddAocItem(cnmt.TitleId, containerPath, $"{ncaId}.nca", true);
+                AddAocItem(cnmt.TitleId, containerPath, $"/{ncaId}.nca", true);
             }
         }
 
@@ -238,7 +238,8 @@ namespace Ryujinx.HLE.FileSystem
                 if (!mergedToContainer)
                 {
                     using FileStream fileStream = File.OpenRead(containerPath);
-                    using PartitionFileSystem partitionFileSystem = new(fileStream.AsStorage());
+                    using PartitionFileSystem partitionFileSystem = new();
+                    partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure();
 
                     _virtualFileSystem.ImportTickets(partitionFileSystem);
                 }
@@ -259,17 +260,17 @@ namespace Ryujinx.HLE.FileSystem
             {
                 var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read);
                 using var ncaFile = new UniqueRef<IFile>();
-                PartitionFileSystem pfs;
 
                 switch (Path.GetExtension(aoc.ContainerPath))
                 {
                     case ".xci":
-                        pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
-                        pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read);
+                        var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
+                        xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
                         break;
                     case ".nsp":
-                        pfs = new PartitionFileSystem(file.AsStorage());
-                        pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read);
+                        var pfs = new PartitionFileSystem();
+                        pfs.Initialize(file.AsStorage());
+                        pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
                         break;
                     default:
                         return false; // Print error?
@@ -606,11 +607,11 @@ namespace Ryujinx.HLE.FileSystem
 
             if (filesystem.FileExists($"{path}/00"))
             {
-                filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode);
+                filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode).ThrowIfFailure();
             }
             else
             {
-                filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode);
+                filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode).ThrowIfFailure();
             }
 
             return file.Release();
diff --git a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
index 807020c60..43bd27761 100644
--- a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
+++ b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
@@ -7,6 +7,7 @@ using LibHac.Fs.Shim;
 using LibHac.FsSrv;
 using LibHac.FsSystem;
 using LibHac.Ncm;
+using LibHac.Sdmmc;
 using LibHac.Spl;
 using LibHac.Tools.Es;
 using LibHac.Tools.Fs;
@@ -32,7 +33,7 @@ namespace Ryujinx.HLE.FileSystem
 
         public KeySet KeySet { get; private set; }
         public EmulatedGameCard GameCard { get; private set; }
-        public EmulatedSdCard SdCard { get; private set; }
+        public SdmmcApi SdCard { get; private set; }
         public ModLoader ModLoader { get; private set; }
 
         private readonly ConcurrentDictionary<ulong, Stream> _romFsByPid;
@@ -198,15 +199,15 @@ namespace Ryujinx.HLE.FileSystem
             fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator();
 
             GameCard = fsServerObjects.GameCard;
-            SdCard = fsServerObjects.SdCard;
+            SdCard = fsServerObjects.Sdmmc;
 
-            SdCard.SetSdCardInsertionStatus(true);
+            SdCard.SetSdCardInserted(true);
 
             var fsServerConfig = new FileSystemServerConfig
             {
-                DeviceOperator = fsServerObjects.DeviceOperator,
                 ExternalKeySet = KeySet.ExternalKeySet,
                 FsCreators = fsServerObjects.FsCreators,
+                StorageDeviceManagerFactory = fsServerObjects.StorageDeviceManagerFactory,
                 RandomGenerator = randomGenerator,
             };
 
@@ -263,7 +264,16 @@ namespace Ryujinx.HLE.FileSystem
 
                 if (result.IsSuccess())
                 {
-                    Ticket ticket = new(ticketFile.Get.AsStream());
+                    // When reading a file from a Sha256PartitionFileSystem, you can't start a read in the middle
+                    // of the hashed portion (usually the first 0x200 bytes) of the file and end the read after
+                    // the end of the hashed portion, so we read the ticket file using a single read.
+                    byte[] ticketData = new byte[0x2C0];
+                    result = ticketFile.Get.Read(out long bytesRead, 0, ticketData);
+
+                    if (result.IsFailure() || bytesRead != ticketData.Length)
+                        continue;
+
+                    Ticket ticket = new(new MemoryStream(ticketData));
                     var titleKey = ticket.GetTitleKey(KeySet);
 
                     if (titleKey != null)
diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs
index b1ba11b59..d52f1815a 100644
--- a/src/Ryujinx.HLE/HLEConfiguration.cs
+++ b/src/Ryujinx.HLE/HLEConfiguration.cs
@@ -101,7 +101,7 @@ namespace Ryujinx.HLE
         /// <summary>
         /// Control if the guest application should be told that there is a Internet connection available.
         /// </summary>
-        internal readonly bool EnableInternetAccess;
+        public bool EnableInternetAccess { internal get; set; }
 
         /// <summary>
         /// Control LibHac's integrity check level.
diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs
index 6706006c3..834bc0595 100644
--- a/src/Ryujinx.HLE/HOS/ModLoader.cs
+++ b/src/Ryujinx.HLE/HOS/ModLoader.cs
@@ -533,7 +533,9 @@ namespace Ryujinx.HLE.HOS
 
             Logger.Info?.Print(LogClass.ModLoader, "Using replacement ExeFS partition");
 
-            exefs = new PartitionFileSystem(mods.ExefsContainers[0].Path.OpenRead().AsStorage());
+            var pfs = new PartitionFileSystem();
+            pfs.Initialize(mods.ExefsContainers[0].Path.OpenRead().AsStorage()).ThrowIfFailure();
+            exefs = pfs;
 
             return true;
         }
diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs
index 599025e3b..1ef52a00d 100644
--- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/FileSystemProxyHelper.cs
@@ -26,7 +26,9 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
             try
             {
                 LocalStorage storage = new(pfsPath, FileAccess.Read, FileMode.Open);
-                using SharedRef<LibHac.Fs.Fsa.IFileSystem> nsp = new(new PartitionFileSystem(storage));
+                var pfs = new PartitionFileSystem();
+                using SharedRef<LibHac.Fs.Fsa.IFileSystem> nsp = new(pfs);
+                pfs.Initialize(storage).ThrowIfFailure();
 
                 ImportTitleKeysFromNsp(nsp.Get, context.Device.System.KeySet);
 
@@ -90,7 +92,8 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
 
                 try
                 {
-                    PartitionFileSystem nsp = new(pfsFile.AsStorage());
+                    PartitionFileSystem nsp = new();
+                    nsp.Initialize(pfsFile.AsStorage()).ThrowIfFailure();
 
                     ImportTitleKeysFromNsp(nsp, context.Device.System.KeySet);
 
diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs
index 4c5c56240..66020d57b 100644
--- a/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Fs/FileSystemProxy/IFileSystem.cs
@@ -1,6 +1,7 @@
 using LibHac;
 using LibHac.Common;
 using LibHac.Fs;
+using LibHac.Fs.Fsa;
 using Path = LibHac.FsSrv.Sf.Path;
 
 namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
@@ -202,6 +203,16 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
             return (ResultCode)result.Value;
         }
 
+        [CommandCmif(16)]
+        public ResultCode GetFileSystemAttribute(ServiceCtx context)
+        {
+            Result result = _fileSystem.Get.GetFileSystemAttribute(out FileSystemAttribute attribute);
+
+            context.ResponseData.Write(SpanHelpers.AsReadOnlyByteSpan(in attribute));
+
+            return (ResultCode)result.Value;
+        }
+
         protected override void Dispose(bool isDisposing)
         {
             if (isDisposing)
diff --git a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
index 644e1a17a..24dd1e9be 100644
--- a/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Fs/IFileSystemProxy.cs
@@ -1380,7 +1380,10 @@ namespace Ryujinx.HLE.HOS.Services.Fs
         [CommandCmif(1016)]
         public ResultCode FlushAccessLogOnSdCard(ServiceCtx context)
         {
-            return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value;
+            // Logging the access log to the SD card isn't implemented, meaning this function will be a no-op since
+            // there's nothing to flush. Return success until it's implemented.
+            // return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value;
+            return ResultCode.Success;
         }
 
         [CommandCmif(1017)]
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
new file mode 100644
index 000000000..80ea2c9d7
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
@@ -0,0 +1,12 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn
+{
+    static class LdnConst
+    {
+        public const int SsidLengthMax = 0x20;
+        public const int AdvertiseDataSizeMax = 0x180;
+        public const int UserNameBytesMax = 0x20;
+        public const int NodeCountMax = 8;
+        public const int StationCountMax = NodeCountMax - 1;
+        public const int PassphraseLengthMax = 0x40;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
index 4b7241c43..5fb2aca05 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
 using System.Runtime.InteropServices;
 
 namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
index c57a7dc45..9d5477931 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
 using System.Runtime.InteropServices;
 
 namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
index f33ceaebe..0461e783e 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
 using System.Runtime.InteropServices;
 
 namespace Ryujinx.HLE.HOS.Services.Ldn.Types
@@ -48,7 +48,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Types
                 {
                     result[i].Reserved = new Array7<byte>();
 
-                    if (i < 8)
+                    if (i < LdnConst.NodeCountMax)
                     {
                         result[i].State = array[i].State;
                         array[i].State = NodeLatestUpdateFlags.None;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
index 85a19a875..5939a1394 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
 using System.Runtime.InteropServices;
 
 namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
index 72db4d41a..764862508 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
 using System.Runtime.InteropServices;
 
 namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
index 1401f5214..3820f936e 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
 using System.Runtime.InteropServices;
 
 namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
index 07bbbeda3..78ebcac82 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
@@ -1,7 +1,6 @@
 using Ryujinx.Common.Memory;
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
 using System;
 
 namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
@@ -30,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
             _parent.NetworkClient.NetworkChange -= NetworkChanged;
         }
 
-        private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+        private void NetworkChanged(object sender, NetworkChangeEventArgs e)
         {
             LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
 
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
similarity index 81%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
index ff342d27c..81825e977 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
@@ -1,12 +1,13 @@
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
 using System;
 
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
 {
     interface INetworkClient : IDisposable
     {
+        bool NeedsRealId { get; }
+
         event EventHandler<NetworkChangeEventArgs> NetworkChange;
 
         void DisconnectNetwork();
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
index 29cc0e1b9..8c6ea66f7 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
@@ -8,7 +8,7 @@ using Ryujinx.Cpu;
 using Ryujinx.HLE.HOS.Ipc;
 using Ryujinx.HLE.HOS.Kernel.Threading;
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm;
 using Ryujinx.Horizon.Common;
 using Ryujinx.Memory;
 using System;
@@ -395,7 +395,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
                             }
                             else
                             {
-                                if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1)
+                                if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
                                 {
                                     // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
                                     ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@@ -546,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
             context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment?
             NetworkConfig networkConfig = context.RequestData.ReadStruct<NetworkConfig>();
 
-            if (networkConfig.IntentId.LocalCommunicationId == -1)
+            if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
             {
                 // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
                 ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@@ -555,7 +555,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
             }
 
             bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId);
-            if (!isLocalCommunicationIdValid)
+            if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
             {
                 return ResultCode.InvalidObject;
             }
@@ -568,13 +568,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
             networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel);
             securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
 
-            if (networkConfig.NodeCountMax <= 8)
+            if (networkConfig.NodeCountMax <= LdnConst.NodeCountMax)
             {
                 if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0)
                 {
                     if (securityConfig.SecurityMode <= SecurityMode.Retail)
                     {
-                        if (securityConfig.Passphrase.Length <= 0x40)
+                        if (securityConfig.Passphrase.Length <= LdnConst.PassphraseLengthMax)
                         {
                             if (_state == NetworkState.AccessPoint)
                             {
@@ -678,7 +678,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
                 return _nifmResultCode;
             }
 
-            if (bufferSize == 0 || bufferSize > 0x180)
+            if (bufferSize == 0 || bufferSize > LdnConst.AdvertiseDataSizeMax)
             {
                 return ResultCode.InvalidArgument;
             }
@@ -848,10 +848,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
 
                 context.Memory.Read(bufferPosition, networkInfoBytes);
 
-                networkInfo = MemoryMarshal.Cast<byte, NetworkInfo>(networkInfoBytes)[0];
+                networkInfo = MemoryMarshal.Read<NetworkInfo>(networkInfoBytes);
             }
 
-            if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1)
+            if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
             {
                 // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
                 ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@@ -860,7 +860,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
             }
 
             bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId);
-            if (!isLocalCommunicationIdValid)
+            if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
             {
                 return ResultCode.InvalidObject;
             }
@@ -1061,10 +1061,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
                     if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
                     {
                         MultiplayerMode mode = context.Device.Configuration.MultiplayerMode;
+
+                        Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initializing with multiplayer mode: {mode}");
+
                         switch (mode)
                         {
+                            case MultiplayerMode.LdnMitm:
+                                NetworkClient = new LdnMitmClient(context.Device.Configuration);
+                                break;
                             case MultiplayerMode.Disabled:
-                                NetworkClient = new DisabledLdnClient();
+                                NetworkClient = new LdnDisabledClient();
                                 break;
                         }
 
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
similarity index 87%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
index 75a1e35ff..e5340b4e9 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
@@ -1,12 +1,13 @@
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
 using System;
 
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
 {
-    class DisabledLdnClient : INetworkClient
+    class LdnDisabledClient : INetworkClient
     {
+        public bool NeedsRealId => true;
+
         public event EventHandler<NetworkChangeEventArgs> NetworkChange;
 
         public NetworkError Connect(ConnectRequest request)
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs
new file mode 100644
index 000000000..8cfd77acb
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs
@@ -0,0 +1,611 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
+{
+    internal class LanDiscovery : IDisposable
+    {
+        private const int DefaultPort = 11452;
+        private const ushort CommonChannel = 6;
+        private const byte CommonLinkLevel = 3;
+        private const byte CommonNetworkType = 2;
+
+        private const int FailureTimeout = 4000;
+
+        private readonly LdnMitmClient _parent;
+        private readonly LanProtocol _protocol;
+        private bool _initialized;
+        private readonly Ssid _fakeSsid;
+        private ILdnTcpSocket _tcp;
+        private LdnProxyUdpServer _udp, _udp2;
+        private readonly List<LdnProxyTcpSession> _stations = new();
+        private readonly object _lock = new();
+
+        private readonly AutoResetEvent _apConnected = new(false);
+
+        internal readonly IPAddress LocalAddr;
+        internal readonly IPAddress LocalBroadcastAddr;
+        internal NetworkInfo NetworkInfo;
+
+        public bool IsHost => _tcp is LdnProxyTcpServer;
+
+        private readonly Random _random = new();
+
+        // NOTE: Credit to https://stackoverflow.com/a/39338188
+        private static IPAddress GetBroadcastAddress(IPAddress address, IPAddress mask)
+        {
+            uint ipAddress = BitConverter.ToUInt32(address.GetAddressBytes(), 0);
+            uint ipMaskV4 = BitConverter.ToUInt32(mask.GetAddressBytes(), 0);
+            uint broadCastIpAddress = ipAddress | ~ipMaskV4;
+
+            return new IPAddress(BitConverter.GetBytes(broadCastIpAddress));
+        }
+
+        private static NetworkInfo GetEmptyNetworkInfo()
+        {
+            NetworkInfo networkInfo = new()
+            {
+                NetworkId = new NetworkId
+                {
+                    SessionId = new Array16<byte>(),
+                },
+                Common = new CommonNetworkInfo
+                {
+                    MacAddress = new Array6<byte>(),
+                    Ssid = new Ssid
+                    {
+                        Name = new Array33<byte>(),
+                    },
+                },
+                Ldn = new LdnNetworkInfo
+                {
+                    NodeCountMax = LdnConst.NodeCountMax,
+                    SecurityParameter = new Array16<byte>(),
+                    Nodes = new Array8<NodeInfo>(),
+                    AdvertiseData = new Array384<byte>(),
+                    Reserved4 = new Array140<byte>(),
+                },
+            };
+
+            for (int i = 0; i < LdnConst.NodeCountMax; i++)
+            {
+                networkInfo.Ldn.Nodes[i] = new NodeInfo
+                {
+                    MacAddress = new Array6<byte>(),
+                    UserName = new Array33<byte>(),
+                    Reserved2 = new Array16<byte>(),
+                };
+            }
+
+            return networkInfo;
+        }
+
+        public LanDiscovery(LdnMitmClient parent, IPAddress ipAddress, IPAddress ipv4Mask)
+        {
+            Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initialize LanDiscovery using IP: {ipAddress}");
+
+            _parent = parent;
+            LocalAddr = ipAddress;
+            LocalBroadcastAddr = GetBroadcastAddress(ipAddress, ipv4Mask);
+
+            _fakeSsid = new Ssid
+            {
+                Length = LdnConst.SsidLengthMax,
+            };
+            _random.NextBytes(_fakeSsid.Name.AsSpan()[..32]);
+
+            _protocol = new LanProtocol(this);
+            _protocol.Accept += OnConnect;
+            _protocol.SyncNetwork += OnSyncNetwork;
+            _protocol.DisconnectStation += DisconnectStation;
+
+            NetworkInfo = GetEmptyNetworkInfo();
+
+            ResetStations();
+
+            if (!InitUdp())
+            {
+                Logger.Error?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Initialize: InitUdp failed.");
+
+                return;
+            }
+
+            _initialized = true;
+        }
+
+        protected void OnSyncNetwork(NetworkInfo info)
+        {
+            bool updated = false;
+
+            lock (_lock)
+            {
+                if (!NetworkInfo.Equals(info))
+                {
+                    NetworkInfo = info;
+                    updated = true;
+
+                    Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"Host IP: {NetworkHelpers.ConvertUint(info.Ldn.Nodes[0].Ipv4Address)}");
+                }
+            }
+
+            if (updated)
+            {
+                _parent.InvokeNetworkChange(info, true);
+            }
+
+            _apConnected.Set();
+        }
+
+        protected void OnConnect(LdnProxyTcpSession station)
+        {
+            lock (_lock)
+            {
+                station.NodeId = LocateEmptyNode();
+
+                if (_stations.Count > LdnConst.StationCountMax || station.NodeId == -1)
+                {
+                    station.Disconnect();
+                    station.Dispose();
+
+                    return;
+                }
+
+                _stations.Add(station);
+
+                UpdateNodes();
+            }
+        }
+
+        public void DisconnectStation(LdnProxyTcpSession station)
+        {
+            if (!station.IsDisposed)
+            {
+                if (station.IsConnected)
+                {
+                    station.Disconnect();
+                }
+
+                station.Dispose();
+            }
+
+            lock (_lock)
+            {
+                if (_stations.Remove(station))
+                {
+                    NetworkInfo.Ldn.Nodes[station.NodeId] = new NodeInfo()
+                    {
+                        MacAddress = new Array6<byte>(),
+                        UserName = new Array33<byte>(),
+                        Reserved2 = new Array16<byte>(),
+                    };
+
+                    UpdateNodes();
+                }
+            }
+        }
+
+        public bool SetAdvertiseData(byte[] data)
+        {
+            if (data.Length > LdnConst.AdvertiseDataSizeMax)
+            {
+                Logger.Error?.PrintMsg(LogClass.ServiceLdn, "AdvertiseData exceeds size limit.");
+
+                return false;
+            }
+
+            data.CopyTo(NetworkInfo.Ldn.AdvertiseData.AsSpan());
+            NetworkInfo.Ldn.AdvertiseDataSize = (ushort)data.Length;
+
+            // NOTE: Otherwise this results in SessionKeepFailed or MasterDisconnected
+            lock (_lock)
+            {
+                if (NetworkInfo.Ldn.Nodes[0].IsConnected == 1)
+                {
+                    UpdateNodes(true);
+                }
+            }
+
+            return true;
+        }
+
+        public void InitNetworkInfo()
+        {
+            lock (_lock)
+            {
+                NetworkInfo.Common.MacAddress = GetFakeMac();
+                NetworkInfo.Common.Channel = CommonChannel;
+                NetworkInfo.Common.LinkLevel = CommonLinkLevel;
+                NetworkInfo.Common.NetworkType = CommonNetworkType;
+                NetworkInfo.Common.Ssid = _fakeSsid;
+
+                NetworkInfo.Ldn.Nodes = new Array8<NodeInfo>();
+
+                for (int i = 0; i < LdnConst.NodeCountMax; i++)
+                {
+                    NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i;
+                    NetworkInfo.Ldn.Nodes[i].IsConnected = 0;
+                }
+            }
+        }
+
+        protected Array6<byte> GetFakeMac(IPAddress address = null)
+        {
+            address ??= LocalAddr;
+
+            byte[] ip = address.GetAddressBytes();
+
+            var macAddress = new Array6<byte>();
+            new byte[] { 0x02, 0x00, ip[0], ip[1], ip[2], ip[3] }.CopyTo(macAddress.AsSpan());
+
+            return macAddress;
+        }
+
+        public bool InitTcp(bool listening, IPAddress address = null, int port = DefaultPort)
+        {
+            Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"LanDiscovery InitTcp: IP: {address}, listening: {listening}");
+
+            if (_tcp != null)
+            {
+                _tcp.DisconnectAndStop();
+                _tcp.Dispose();
+                _tcp = null;
+            }
+
+            ILdnTcpSocket tcpSocket;
+
+            if (listening)
+            {
+                try
+                {
+                    address ??= LocalAddr;
+
+                    tcpSocket = new LdnProxyTcpServer(_protocol, address, port);
+                }
+                catch (Exception ex)
+                {
+                    Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpServer: {ex}");
+
+                    return false;
+                }
+
+                if (!tcpSocket.Start())
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                if (address == null)
+                {
+                    return false;
+                }
+
+                try
+                {
+                    tcpSocket = new LdnProxyTcpClient(_protocol, address, port);
+                }
+                catch (Exception ex)
+                {
+                    Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpClient: {ex}");
+
+                    return false;
+                }
+            }
+
+            _tcp = tcpSocket;
+
+            return true;
+        }
+
+        public bool InitUdp()
+        {
+            _udp?.Stop();
+            _udp2?.Stop();
+
+            try
+            {
+                // NOTE: Linux won't receive any broadcast packets if the socket is not bound to the broadcast address.
+                //       Windows only works if bound to localhost or the local address.
+                //       See this discussion: https://stackoverflow.com/questions/13666789/receiving-udp-broadcast-packets-on-linux
+                if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+                {
+                    _udp2 = new LdnProxyUdpServer(_protocol, LocalBroadcastAddr, DefaultPort);
+                }
+
+                _udp = new LdnProxyUdpServer(_protocol, LocalAddr, DefaultPort);
+            }
+            catch (Exception ex)
+            {
+                Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyUdpServer: {ex}");
+
+                return false;
+            }
+
+            return true;
+        }
+
+        public NetworkInfo[] Scan(ushort channel, ScanFilter filter)
+        {
+            _udp.ClearScanResults();
+
+            if (_protocol.SendBroadcast(_udp, LanPacketType.Scan, DefaultPort) < 0)
+            {
+                return Array.Empty<NetworkInfo>();
+            }
+
+            List<NetworkInfo> outNetworkInfo = new();
+
+            foreach (KeyValuePair<ulong, NetworkInfo> item in _udp.GetScanResults())
+            {
+                bool copy = true;
+
+                if (filter.Flag.HasFlag(ScanFilterFlag.LocalCommunicationId))
+                {
+                    copy &= filter.NetworkId.IntentId.LocalCommunicationId == item.Value.NetworkId.IntentId.LocalCommunicationId;
+                }
+
+                if (filter.Flag.HasFlag(ScanFilterFlag.SessionId))
+                {
+                    copy &= filter.NetworkId.SessionId.AsSpan().SequenceEqual(item.Value.NetworkId.SessionId.AsSpan());
+                }
+
+                if (filter.Flag.HasFlag(ScanFilterFlag.NetworkType))
+                {
+                    copy &= filter.NetworkType == (NetworkType)item.Value.Common.NetworkType;
+                }
+
+                if (filter.Flag.HasFlag(ScanFilterFlag.Ssid))
+                {
+                    Span<byte> gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..];
+                    Span<byte> scanSsid = filter.Ssid.Name.AsSpan()[filter.Ssid.Length..];
+                    copy &= gameSsid.SequenceEqual(scanSsid);
+                }
+
+                if (filter.Flag.HasFlag(ScanFilterFlag.SceneId))
+                {
+                    copy &= filter.NetworkId.IntentId.SceneId == item.Value.NetworkId.IntentId.SceneId;
+                }
+
+                if (copy)
+                {
+                    if (item.Value.Ldn.Nodes[0].UserName[0] != 0)
+                    {
+                        outNetworkInfo.Add(item.Value);
+                    }
+                    else
+                    {
+                        Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Scan: Got empty Username. There might be a timing issue somewhere...");
+                    }
+                }
+            }
+
+            return outNetworkInfo.ToArray();
+        }
+
+        protected void ResetStations()
+        {
+            lock (_lock)
+            {
+                foreach (LdnProxyTcpSession station in _stations)
+                {
+                    station.Disconnect();
+                    station.Dispose();
+                }
+
+                _stations.Clear();
+            }
+        }
+
+        private int LocateEmptyNode()
+        {
+            Array8<NodeInfo> nodes = NetworkInfo.Ldn.Nodes;
+
+            for (int i = 1; i < nodes.Length; i++)
+            {
+                if (nodes[i].IsConnected == 0)
+                {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        protected void UpdateNodes(bool forceUpdate = false)
+        {
+            int countConnected = 1;
+
+            foreach (LdnProxyTcpSession station in _stations.Where(station => station.IsConnected))
+            {
+                countConnected++;
+
+                station.OverrideInfo();
+
+                // NOTE: This is not part of the original implementation.
+                NetworkInfo.Ldn.Nodes[station.NodeId] = station.NodeInfo;
+            }
+
+            byte nodeCount = (byte)countConnected;
+
+            bool networkInfoChanged = forceUpdate || NetworkInfo.Ldn.NodeCount != nodeCount;
+
+            NetworkInfo.Ldn.NodeCount = nodeCount;
+
+            foreach (LdnProxyTcpSession station in _stations)
+            {
+                if (station.IsConnected)
+                {
+                    if (_protocol.SendPacket(station, LanPacketType.SyncNetwork, SpanHelpers.AsSpan<NetworkInfo, byte>(ref NetworkInfo).ToArray()) < 0)
+                    {
+                        Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to send {LanPacketType.SyncNetwork} to station {station.NodeId}");
+                    }
+                }
+            }
+
+            if (networkInfoChanged)
+            {
+                _parent.InvokeNetworkChange(NetworkInfo, true);
+            }
+        }
+
+        protected NodeInfo GetNodeInfo(NodeInfo node, UserConfig userConfig, ushort localCommunicationVersion)
+        {
+            uint ipAddress = NetworkHelpers.ConvertIpv4Address(LocalAddr);
+
+            node.MacAddress = GetFakeMac();
+            node.IsConnected = 1;
+            node.UserName = userConfig.UserName;
+            node.LocalCommunicationVersion = localCommunicationVersion;
+            node.Ipv4Address = ipAddress;
+
+            return node;
+        }
+
+        public bool CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig)
+        {
+            if (!InitTcp(true))
+            {
+                return false;
+            }
+
+            InitNetworkInfo();
+
+            NetworkInfo.Ldn.NodeCountMax = networkConfig.NodeCountMax;
+            NetworkInfo.Ldn.SecurityMode = (ushort)securityConfig.SecurityMode;
+
+            NetworkInfo.Common.Channel = networkConfig.Channel == 0 ? (ushort)6 : networkConfig.Channel;
+
+            NetworkInfo.NetworkId.SessionId = new Array16<byte>();
+            _random.NextBytes(NetworkInfo.NetworkId.SessionId.AsSpan());
+            NetworkInfo.NetworkId.IntentId = networkConfig.IntentId;
+
+            NetworkInfo.Ldn.Nodes[0] = GetNodeInfo(NetworkInfo.Ldn.Nodes[0], userConfig, networkConfig.LocalCommunicationVersion);
+            NetworkInfo.Ldn.Nodes[0].IsConnected = 1;
+            NetworkInfo.Ldn.NodeCount++;
+
+            _parent.InvokeNetworkChange(NetworkInfo, true);
+
+            return true;
+        }
+
+        public void DestroyNetwork()
+        {
+            if (_tcp != null)
+            {
+                try
+                {
+                    _tcp.DisconnectAndStop();
+                }
+                finally
+                {
+                    _tcp.Dispose();
+                    _tcp = null;
+                }
+            }
+
+            ResetStations();
+        }
+
+        public NetworkError Connect(NetworkInfo networkInfo, UserConfig userConfig, uint localCommunicationVersion)
+        {
+            _apConnected.Reset();
+
+            if (networkInfo.Ldn.NodeCount == 0)
+            {
+                return NetworkError.Unknown;
+            }
+
+            IPAddress address = NetworkHelpers.ConvertUint(networkInfo.Ldn.Nodes[0].Ipv4Address);
+
+            Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Connecting to host: {address}");
+
+            if (!InitTcp(false, address))
+            {
+                Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Could not initialize TCPClient");
+
+                return NetworkError.ConnectNotFound;
+            }
+
+            if (!_tcp.Connect())
+            {
+                Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Failed to connect.");
+
+                return NetworkError.ConnectFailure;
+            }
+
+            NodeInfo myNode = GetNodeInfo(new NodeInfo(), userConfig, (ushort)localCommunicationVersion);
+            if (_protocol.SendPacket(_tcp, LanPacketType.Connect, SpanHelpers.AsSpan<NodeInfo, byte>(ref myNode).ToArray()) < 0)
+            {
+                return NetworkError.Unknown;
+            }
+
+            return _apConnected.WaitOne(FailureTimeout) ? NetworkError.None : NetworkError.ConnectTimeout;
+        }
+
+        public void Dispose()
+        {
+            if (_initialized)
+            {
+                DisconnectAndStop();
+                ResetStations();
+                _initialized = false;
+            }
+
+            _protocol.Accept -= OnConnect;
+            _protocol.SyncNetwork -= OnSyncNetwork;
+            _protocol.DisconnectStation -= DisconnectStation;
+        }
+
+        public void DisconnectAndStop()
+        {
+            if (_udp != null)
+            {
+                try
+                {
+                    _udp.Stop();
+                }
+                finally
+                {
+                    _udp.Dispose();
+                    _udp = null;
+                }
+            }
+
+            if (_udp2 != null)
+            {
+                try
+                {
+                    _udp2.Stop();
+                }
+                finally
+                {
+                    _udp2.Dispose();
+                    _udp2 = null;
+                }
+            }
+
+            if (_tcp != null)
+            {
+                try
+                {
+                    _tcp.DisconnectAndStop();
+                }
+                finally
+                {
+                    _tcp.Dispose();
+                    _tcp = null;
+                }
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs
new file mode 100644
index 000000000..f22e430bd
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs
@@ -0,0 +1,314 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
+{
+    internal class LanProtocol
+    {
+        private const uint LanMagic = 0x11451400;
+
+        public const int BufferSize = 2048;
+        public const int TcpTxBufferSize = 0x800;
+        public const int TcpRxBufferSize = 0x1000;
+        public const int TxBufferSizeMax = 0x2000;
+        public const int RxBufferSizeMax = 0x2000;
+
+        private readonly int _headerSize = Marshal.SizeOf<LanPacketHeader>();
+
+        private readonly LanDiscovery _discovery;
+
+        public event Action<LdnProxyTcpSession> Accept;
+        public event Action<EndPoint, LanPacketType, byte[]> Scan;
+        public event Action<NetworkInfo> ScanResponse;
+        public event Action<NetworkInfo> SyncNetwork;
+        public event Action<NodeInfo, EndPoint> Connect;
+        public event Action<LdnProxyTcpSession> DisconnectStation;
+
+        public LanProtocol(LanDiscovery parent)
+        {
+            _discovery = parent;
+        }
+
+        public void InvokeAccept(LdnProxyTcpSession session)
+        {
+            Accept?.Invoke(session);
+        }
+
+        public void InvokeDisconnectStation(LdnProxyTcpSession session)
+        {
+            DisconnectStation?.Invoke(session);
+        }
+
+        private void DecodeAndHandle(LanPacketHeader header, byte[] data, EndPoint endPoint = null)
+        {
+            switch (header.Type)
+            {
+                case LanPacketType.Scan:
+                    // UDP
+                    if (_discovery.IsHost)
+                    {
+                        Scan?.Invoke(endPoint, LanPacketType.ScanResponse, SpanHelpers.AsSpan<NetworkInfo, byte>(ref _discovery.NetworkInfo).ToArray());
+                    }
+                    break;
+                case LanPacketType.ScanResponse:
+                    // UDP
+                    ScanResponse?.Invoke(MemoryMarshal.Cast<byte, NetworkInfo>(data)[0]);
+                    break;
+                case LanPacketType.SyncNetwork:
+                    // TCP
+                    SyncNetwork?.Invoke(MemoryMarshal.Cast<byte, NetworkInfo>(data)[0]);
+                    break;
+                case LanPacketType.Connect:
+                    // TCP Session / Station
+                    Connect?.Invoke(MemoryMarshal.Cast<byte, NodeInfo>(data)[0], endPoint);
+                    break;
+                default:
+                    Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decode error: Unhandled type {header.Type}");
+                    break;
+            }
+        }
+
+        public void Read(scoped ref byte[] buffer, scoped ref int bufferEnd, byte[] data, int offset, int size, EndPoint endPoint = null)
+        {
+            if (endPoint != null && _discovery.LocalAddr.Equals(((IPEndPoint)endPoint).Address))
+            {
+                return;
+            }
+
+            int index = 0;
+            while (index < size)
+            {
+                if (bufferEnd < _headerSize)
+                {
+                    int copyable2 = Math.Min(size - index, Math.Min(size, _headerSize - bufferEnd));
+
+                    Array.Copy(data, index + offset, buffer, bufferEnd, copyable2);
+
+                    index += copyable2;
+                    bufferEnd += copyable2;
+                }
+
+                if (bufferEnd >= _headerSize)
+                {
+                    LanPacketHeader header = MemoryMarshal.Cast<byte, LanPacketHeader>(buffer)[0];
+                    if (header.Magic != LanMagic)
+                    {
+                        bufferEnd = 0;
+
+                        Logger.Warning?.PrintMsg(LogClass.ServiceLdn, $"Invalid magic number in received packet. [magic: {header.Magic}] [EP: {endPoint}]");
+
+                        return;
+                    }
+
+                    int totalSize = _headerSize + header.Length;
+                    if (totalSize > BufferSize)
+                    {
+                        bufferEnd = 0;
+
+                        Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Max packet size {BufferSize} exceeded.");
+
+                        return;
+                    }
+
+                    int copyable = Math.Min(size - index, Math.Min(size, totalSize - bufferEnd));
+
+                    Array.Copy(data, index + offset, buffer, bufferEnd, copyable);
+
+                    index += copyable;
+                    bufferEnd += copyable;
+
+                    if (totalSize == bufferEnd)
+                    {
+                        byte[] ldnData = new byte[totalSize - _headerSize];
+                        Array.Copy(buffer, _headerSize, ldnData, 0, ldnData.Length);
+
+                        if (header.Compressed == 1)
+                        {
+                            if (Decompress(ldnData, out byte[] decompressedLdnData) != 0)
+                            {
+                                Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error:\n {header}, {_headerSize}\n {ldnData}, {ldnData.Length}");
+
+                                return;
+                            }
+
+                            if (decompressedLdnData.Length != header.DecompressLength)
+                            {
+                                Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error: length does not match. ({decompressedLdnData.Length} != {header.DecompressLength})");
+                                Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error data: '{string.Join("", decompressedLdnData.Select(x => (int)x).ToArray())}'");
+
+                                return;
+                            }
+
+                            ldnData = decompressedLdnData;
+                        }
+
+                        DecodeAndHandle(header, ldnData, endPoint);
+
+                        bufferEnd = 0;
+                    }
+                }
+            }
+        }
+
+        public int SendBroadcast(ILdnSocket s, LanPacketType type, int port)
+        {
+            return SendPacket(s, type, Array.Empty<byte>(), new IPEndPoint(_discovery.LocalBroadcastAddr, port));
+        }
+
+        public int SendPacket(ILdnSocket s, LanPacketType type, byte[] data, EndPoint endPoint = null)
+        {
+            byte[] buf = PreparePacket(type, data);
+
+            return s.SendPacketAsync(endPoint, buf) ? 0 : -1;
+        }
+
+        public int SendPacket(LdnProxyTcpSession s, LanPacketType type, byte[] data)
+        {
+            byte[] buf = PreparePacket(type, data);
+
+            return s.SendAsync(buf) ? 0 : -1;
+        }
+
+        private LanPacketHeader PrepareHeader(LanPacketHeader header, LanPacketType type)
+        {
+            header.Magic = LanMagic;
+            header.Type = type;
+            header.Compressed = 0;
+            header.Length = 0;
+            header.DecompressLength = 0;
+            header.Reserved = new Array2<byte>();
+
+            return header;
+        }
+
+        private byte[] PreparePacket(LanPacketType type, byte[] data)
+        {
+            LanPacketHeader header = PrepareHeader(new LanPacketHeader(), type);
+            header.Length = (ushort)data.Length;
+
+            byte[] buf;
+            if (data.Length > 0)
+            {
+                if (Compress(data, out byte[] compressed) == 0)
+                {
+                    header.DecompressLength = header.Length;
+                    header.Length = (ushort)compressed.Length;
+                    header.Compressed = 1;
+
+                    buf = new byte[compressed.Length + _headerSize];
+
+                    SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
+                    compressed.CopyTo(buf, _headerSize);
+                }
+                else
+                {
+                    buf = new byte[data.Length + _headerSize];
+
+                    Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Compressing packet data failed.");
+
+                    SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
+                    data.CopyTo(buf, _headerSize);
+                }
+            }
+            else
+            {
+                buf = new byte[_headerSize];
+                SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
+            }
+
+            return buf;
+        }
+
+        private int Compress(byte[] input, out byte[] output)
+        {
+            List<byte> outputList = new();
+            int i = 0;
+            int maxCount = 0xFF;
+
+            while (i < input.Length)
+            {
+                byte inputByte = input[i++];
+                int count = 0;
+
+                if (inputByte == 0)
+                {
+                    while (i < input.Length && input[i] == 0 && count < maxCount)
+                    {
+                        count += 1;
+                        i++;
+                    }
+                }
+
+                if (inputByte == 0)
+                {
+                    outputList.Add(0);
+
+                    if (outputList.Count == BufferSize)
+                    {
+                        output = null;
+
+                        return -1;
+                    }
+
+                    outputList.Add((byte)count);
+                }
+                else
+                {
+                    outputList.Add(inputByte);
+                }
+            }
+
+            output = outputList.ToArray();
+
+            return i == input.Length ? 0 : -1;
+        }
+
+        private int Decompress(byte[] input, out byte[] output)
+        {
+            List<byte> outputList = new();
+            int i = 0;
+
+            while (i < input.Length && outputList.Count < BufferSize)
+            {
+                byte inputByte = input[i++];
+
+                outputList.Add(inputByte);
+
+                if (inputByte == 0)
+                {
+                    if (i == input.Length)
+                    {
+                        output = null;
+
+                        return -1;
+                    }
+
+                    int count = input[i++];
+
+                    for (int j = 0; j < count; j++)
+                    {
+                        if (outputList.Count == BufferSize)
+                        {
+                            break;
+                        }
+
+                        outputList.Add(inputByte);
+                    }
+                }
+            }
+
+            output = outputList.ToArray();
+
+            return i == input.Length ? 0 : -1;
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs
new file mode 100644
index 000000000..068013053
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs
@@ -0,0 +1,104 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using System;
+using System.Net.NetworkInformation;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
+{
+    /// <summary>
+    /// Client implementation for <a href="https://github.com/spacemeowx2/ldn_mitm">ldn_mitm</a>
+    /// </summary>
+    internal class LdnMitmClient : INetworkClient
+    {
+        public bool NeedsRealId => false;
+
+        public event EventHandler<NetworkChangeEventArgs> NetworkChange;
+
+        private readonly LanDiscovery _lanDiscovery;
+
+        public LdnMitmClient(HLEConfiguration config)
+        {
+            UnicastIPAddressInformation localIpInterface = NetworkHelpers.GetLocalInterface(config.MultiplayerLanInterfaceId).Item2;
+
+            _lanDiscovery = new LanDiscovery(this, localIpInterface.Address, localIpInterface.IPv4Mask);
+        }
+
+        internal void InvokeNetworkChange(NetworkInfo info, bool connected, DisconnectReason reason = DisconnectReason.None)
+        {
+            NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, connected: connected, disconnectReason: reason));
+        }
+
+        public NetworkError Connect(ConnectRequest request)
+        {
+            return _lanDiscovery.Connect(request.NetworkInfo, request.UserConfig, request.LocalCommunicationVersion);
+        }
+
+        public NetworkError ConnectPrivate(ConnectPrivateRequest request)
+        {
+            // NOTE: This method is not implemented in ldn_mitm
+            Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient ConnectPrivate");
+
+            return NetworkError.None;
+        }
+
+        public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
+        {
+            return _lanDiscovery.CreateNetwork(request.SecurityConfig, request.UserConfig, request.NetworkConfig);
+        }
+
+        public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
+        {
+            // NOTE: This method is not implemented in ldn_mitm
+            Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient CreateNetworkPrivate");
+
+            return true;
+        }
+
+        public void DisconnectAndStop()
+        {
+            _lanDiscovery.DisconnectAndStop();
+        }
+
+        public void DisconnectNetwork()
+        {
+            _lanDiscovery.DestroyNetwork();
+        }
+
+        public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
+        {
+            // NOTE: This method is not implemented in ldn_mitm
+            Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient Reject");
+
+            return ResultCode.Success;
+        }
+
+        public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
+        {
+            return _lanDiscovery.Scan(channel, scanFilter);
+        }
+
+        public void SetAdvertiseData(byte[] data)
+        {
+            _lanDiscovery.SetAdvertiseData(data);
+        }
+
+        public void SetGameVersion(byte[] versionString)
+        {
+            // NOTE: This method is not implemented in ldn_mitm
+            Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetGameVersion");
+        }
+
+        public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
+        {
+            // NOTE: This method is not implemented in ldn_mitm
+            Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetStationAcceptPolicy");
+        }
+
+        public void Dispose()
+        {
+            _lanDiscovery.Dispose();
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs
new file mode 100644
index 000000000..b6e6cea9e
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Net;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+    internal interface ILdnSocket : IDisposable
+    {
+        bool SendPacketAsync(EndPoint endpoint, byte[] buffer);
+        bool Start();
+        bool Stop();
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs
new file mode 100644
index 000000000..97e3bd627
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+    internal interface ILdnTcpSocket : ILdnSocket
+    {
+        bool Connect();
+        void DisconnectAndStop();
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs
new file mode 100644
index 000000000..cfe9a8aae
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs
@@ -0,0 +1,99 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+    internal class LdnProxyTcpClient : NetCoreServer.TcpClient, ILdnTcpSocket
+    {
+        private readonly LanProtocol _protocol;
+        private byte[] _buffer;
+        private int _bufferEnd;
+
+        public LdnProxyTcpClient(LanProtocol protocol, IPAddress address, int port) : base(address, port)
+        {
+            _protocol = protocol;
+            _buffer = new byte[LanProtocol.BufferSize];
+            OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
+            OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
+            OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
+            OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
+        }
+
+        protected override void OnConnected()
+        {
+            Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient connected!");
+        }
+
+        protected override void OnReceived(byte[] buffer, long offset, long size)
+        {
+            _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size);
+        }
+
+        public void DisconnectAndStop()
+        {
+            DisconnectAsync();
+
+            while (IsConnected)
+            {
+                Thread.Yield();
+            }
+        }
+
+        public bool SendPacketAsync(EndPoint endPoint, byte[] data)
+        {
+            if (endPoint != null)
+            {
+                Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTcpClient is sending a packet but endpoint is not null.");
+            }
+
+            if (IsConnecting && !IsConnected)
+            {
+                Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPClient needs to connect before sending packets. Waiting...");
+
+                while (IsConnecting && !IsConnected)
+                {
+                    Thread.Yield();
+                }
+            }
+
+            return SendAsync(data);
+        }
+
+        protected override void OnError(SocketError error)
+        {
+            Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient caught an error with code {error}");
+        }
+
+        protected override void Dispose(bool disposingManagedResources)
+        {
+            DisconnectAndStop();
+            base.Dispose(disposingManagedResources);
+        }
+
+        public override bool Connect()
+        {
+            // TODO: NetCoreServer has a Connect() method, but it currently leads to weird issues.
+            base.ConnectAsync();
+
+            while (IsConnecting)
+            {
+                Thread.Sleep(1);
+            }
+
+            return IsConnected;
+        }
+
+        public bool Start()
+        {
+            throw new InvalidOperationException("Start was called.");
+        }
+
+        public bool Stop()
+        {
+            throw new InvalidOperationException("Stop was called.");
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs
new file mode 100644
index 000000000..0ca12b9f6
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs
@@ -0,0 +1,54 @@
+using NetCoreServer;
+using Ryujinx.Common.Logging;
+using System;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+    internal class LdnProxyTcpServer : TcpServer, ILdnTcpSocket
+    {
+        private readonly LanProtocol _protocol;
+
+        public LdnProxyTcpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
+        {
+            _protocol = protocol;
+            OptionReuseAddress = true;
+            OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
+            OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
+
+            Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer created a server for this address: {address}:{port}");
+        }
+
+        protected override TcpSession CreateSession()
+        {
+            return new LdnProxyTcpSession(this, _protocol);
+        }
+
+        protected override void OnError(SocketError error)
+        {
+            Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer caught an error with code {error}");
+        }
+
+        protected override void Dispose(bool disposingManagedResources)
+        {
+            Stop();
+            base.Dispose(disposingManagedResources);
+        }
+
+        public bool Connect()
+        {
+            throw new InvalidOperationException("Connect was called.");
+        }
+
+        public void DisconnectAndStop()
+        {
+            Stop();
+        }
+
+        public bool SendPacketAsync(EndPoint endpoint, byte[] buffer)
+        {
+            throw new InvalidOperationException("SendPacketAsync was called.");
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs
new file mode 100644
index 000000000..f30c4b011
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs
@@ -0,0 +1,83 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+    internal class LdnProxyTcpSession : NetCoreServer.TcpSession
+    {
+        private readonly LanProtocol _protocol;
+
+        internal int NodeId;
+        internal NodeInfo NodeInfo;
+
+        private byte[] _buffer;
+        private int _bufferEnd;
+
+        public LdnProxyTcpSession(LdnProxyTcpServer server, LanProtocol protocol) : base(server)
+        {
+            _protocol = protocol;
+            _protocol.Connect += OnConnect;
+            _buffer = new byte[LanProtocol.BufferSize];
+            OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
+            OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
+            OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
+            OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
+        }
+
+        public void OverrideInfo()
+        {
+            NodeInfo.NodeId = (byte)NodeId;
+            NodeInfo.IsConnected = (byte)(IsConnected ? 1 : 0);
+        }
+
+        protected override void OnConnected()
+        {
+            Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession connected!");
+        }
+
+        protected override void OnDisconnected()
+        {
+            Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession disconnected!");
+
+            _protocol.InvokeDisconnectStation(this);
+        }
+
+        protected override void OnReceived(byte[] buffer, long offset, long size)
+        {
+            _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, this.Socket.RemoteEndPoint);
+        }
+
+        protected override void OnError(SocketError error)
+        {
+            Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession caught an error with code {error}");
+
+            Dispose();
+        }
+
+        protected override void Dispose(bool disposingManagedResources)
+        {
+            _protocol.Connect -= OnConnect;
+            base.Dispose(disposingManagedResources);
+        }
+
+        private void OnConnect(NodeInfo info, EndPoint endPoint)
+        {
+            try
+            {
+                if (endPoint.Equals(this.Socket.RemoteEndPoint))
+                {
+                    NodeInfo = info;
+                    _protocol.InvokeAccept(this);
+                }
+            }
+            catch (System.ObjectDisposedException)
+            {
+                Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession was disposed. [IP: {NodeInfo.Ipv4Address}]");
+
+                _protocol.InvokeDisconnectStation(this);
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs
new file mode 100644
index 000000000..b1519d1ff
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs
@@ -0,0 +1,157 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+    internal class LdnProxyUdpServer : NetCoreServer.UdpServer, ILdnSocket
+    {
+        private const long ScanFrequency = 1000;
+
+        private readonly LanProtocol _protocol;
+        private byte[] _buffer;
+        private int _bufferEnd;
+
+        private readonly object _scanLock = new();
+
+        private Dictionary<ulong, NetworkInfo> _scanResultsLast = new();
+        private Dictionary<ulong, NetworkInfo> _scanResults = new();
+        private readonly AutoResetEvent _scanResponse = new(false);
+        private long _lastScanTime;
+
+        public LdnProxyUdpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
+        {
+            _protocol = protocol;
+            _protocol.Scan += HandleScan;
+            _protocol.ScanResponse += HandleScanResponse;
+            _buffer = new byte[LanProtocol.BufferSize];
+            OptionReuseAddress = true;
+            OptionReceiveBufferSize = LanProtocol.RxBufferSizeMax;
+            OptionSendBufferSize = LanProtocol.TxBufferSizeMax;
+
+            Start();
+        }
+
+        protected override Socket CreateSocket()
+        {
+            return new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp)
+            {
+                EnableBroadcast = true,
+            };
+        }
+
+        protected override void OnStarted()
+        {
+            ReceiveAsync();
+        }
+
+        protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size)
+        {
+            _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, endpoint);
+            ReceiveAsync();
+        }
+
+        protected override void OnError(SocketError error)
+        {
+            Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyUdpServer caught an error with code {error}");
+        }
+
+        protected override void Dispose(bool disposingManagedResources)
+        {
+            _protocol.Scan -= HandleScan;
+            _protocol.ScanResponse -= HandleScanResponse;
+
+            _scanResponse.Dispose();
+
+            base.Dispose(disposingManagedResources);
+        }
+
+        public bool SendPacketAsync(EndPoint endpoint, byte[] data)
+        {
+            return SendAsync(endpoint, data);
+        }
+
+        private void HandleScan(EndPoint endpoint, LanPacketType type, byte[] data)
+        {
+            _protocol.SendPacket(this, type, data, endpoint);
+        }
+
+        private void HandleScanResponse(NetworkInfo info)
+        {
+            Span<byte> mac = stackalloc byte[8];
+
+            info.Common.MacAddress.AsSpan().CopyTo(mac);
+
+            lock (_scanLock)
+            {
+                _scanResults[BitConverter.ToUInt64(mac)] = info;
+
+                _scanResponse.Set();
+            }
+        }
+
+        public void ClearScanResults()
+        {
+            // Rate limit scans.
+
+            long timeMs = Stopwatch.GetTimestamp() / (Stopwatch.Frequency / 1000);
+            long delay = ScanFrequency - (timeMs - _lastScanTime);
+
+            if (delay > 0)
+            {
+                Thread.Sleep((int)delay);
+            }
+
+            _lastScanTime = timeMs;
+
+            lock (_scanLock)
+            {
+                var newResults = _scanResultsLast;
+                newResults.Clear();
+
+                _scanResultsLast = _scanResults;
+                _scanResults = newResults;
+
+                _scanResponse.Reset();
+            }
+        }
+
+        public Dictionary<ulong, NetworkInfo> GetScanResults()
+        {
+            // NOTE: Try to minimize waiting time for scan results.
+            // After we receive the first response, wait a short time for follow-ups and return.
+            // Responses that were too late to catch will appear in the next scan.
+
+            // ldn_mitm does not do this, but this improves latency for games that expect it to be low (it is on console).
+
+            if (_scanResponse.WaitOne(1000))
+            {
+                // Wait a short while longer in case there are some other responses.
+                Thread.Sleep(33);
+            }
+
+            lock (_scanLock)
+            {
+                var results = new Dictionary<ulong, NetworkInfo>();
+
+                foreach (KeyValuePair<ulong, NetworkInfo> last in _scanResultsLast)
+                {
+                    results[last.Key] = last.Value;
+                }
+
+                foreach (KeyValuePair<ulong, NetworkInfo> scan in _scanResults)
+                {
+                    results[scan.Key] = scan.Value;
+                }
+
+                return results;
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs
new file mode 100644
index 000000000..4cebe414d
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
+{
+    [StructLayout(LayoutKind.Sequential, Size = 12)]
+    internal struct LanPacketHeader
+    {
+        public uint Magic;
+        public LanPacketType Type;
+        public byte Compressed;
+        public ushort Length;
+        public ushort DecompressLength;
+        public Array2<byte> Reserved;
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs
new file mode 100644
index 000000000..901f00b00
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
+{
+    internal enum LanPacketType : byte
+    {
+        Scan,
+        ScanResponse,
+        Connect,
+        SyncNetwork,
+    }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs
similarity index 91%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs
index 1cc09c00d..b379d2680 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs
@@ -1,7 +1,7 @@
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
 using System;
 
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
 {
     class NetworkChangeEventArgs : EventArgs
     {
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
index c190d6ed1..e39c01978 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
@@ -1,7 +1,6 @@
 using Ryujinx.Common.Memory;
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
 using System;
 
 namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
@@ -22,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
             _parent.NetworkClient.NetworkChange += NetworkChanged;
         }
 
-        private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+        private void NetworkChanged(object sender, NetworkChangeEventArgs e)
         {
             LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
 
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs
similarity index 86%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs
index 47e48d0a1..058ce62d0 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs
@@ -1,7 +1,7 @@
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
 using System.Runtime.InteropServices;
 
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
 {
     [StructLayout(LayoutKind.Sequential, Size = 0xBC)]
     struct ConnectPrivateRequest
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs
similarity index 84%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs
index 9ff46cccb..136589b2a 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs
@@ -1,7 +1,7 @@
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
 using System.Runtime.InteropServices;
 
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
 {
     [StructLayout(LayoutKind.Sequential, Size = 0x4FC)]
     struct ConnectRequest
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
similarity index 88%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
index 6e890618c..ec0668884 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
@@ -1,7 +1,7 @@
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
 using System.Runtime.InteropServices;
 
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
 {
     /// <remarks>
     /// Advertise data is appended separately (remaining data in the buffer).
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
similarity index 86%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
index 4efe9165a..eecea5eb0 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
@@ -1,7 +1,7 @@
 using Ryujinx.HLE.HOS.Services.Ldn.Types;
 using System.Runtime.InteropServices;
 
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
 {
     /// <remarks>
     /// Advertise data is appended separately (remaining data in the buffer).
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs
similarity index 80%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs
index 70ebf7e38..cd576e055 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs
@@ -1,4 +1,4 @@
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
 {
     enum NetworkError : int
     {
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs
similarity index 71%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs
index acb0b36ac..7e0c2a43f 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs
@@ -1,6 +1,6 @@
 using System.Runtime.InteropServices;
 
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
 {
     [StructLayout(LayoutKind.Sequential, Size = 0x4)]
     struct NetworkErrorMessage
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
index 6de99131e..50f7d5853 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
@@ -20,7 +20,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
         private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
         private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 
-        internal static (bool, ProcessResult) TryLoad(this PartitionFileSystem partitionFileSystem, Switch device, string path, out string errorMessage)
+        internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, string path, out string errorMessage)
+            where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
+            where TFormat : IPartitionFileSystemFormat
+            where THeader : unmanaged, IPartitionFileSystemHeader
+            where TEntry : unmanaged, IPartitionFileSystemEntry
         {
             errorMessage = null;
 
@@ -91,7 +95,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
                         string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
                         if (File.Exists(updatePath))
                         {
-                            PartitionFileSystem updatePartitionFileSystem = new(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage());
+                            PartitionFileSystem updatePartitionFileSystem = new();
+                            updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure();
 
                             device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
 
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
index 51cbb6f99..220b868db 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
@@ -69,7 +69,8 @@ namespace Ryujinx.HLE.Loaders.Processes
         public bool LoadNsp(string path)
         {
             FileStream file = new(path, FileMode.Open, FileAccess.Read);
-            PartitionFileSystem partitionFileSystem = new(file.AsStorage());
+            PartitionFileSystem partitionFileSystem = new();
+            partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
 
             (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage);
 
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
index 292a5c122..c229b1742 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
@@ -1,8 +1,8 @@
 using LibHac.Account;
 using LibHac.Common;
 using LibHac.Fs;
+using LibHac.Fs.Fsa;
 using LibHac.Fs.Shim;
-using LibHac.FsSystem;
 using LibHac.Loader;
 using LibHac.Ncm;
 using LibHac.Ns;
@@ -33,7 +33,7 @@ namespace Ryujinx.HLE.Loaders.Processes
         // TODO: Remove this workaround when ASLR is implemented.
         private const ulong CodeStartOffset = 0x500000UL;
 
-        public static LibHac.Result RegisterProgramMapInfo(Switch device, PartitionFileSystem partitionFileSystem)
+        public static LibHac.Result RegisterProgramMapInfo(Switch device, IFileSystem partitionFileSystem)
         {
             ulong applicationId = 0;
             int programCount = 0;
diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
index 5e3aa0eac..f3439cc8f 100644
--- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -27,6 +27,7 @@
     <PackageReference Include="SixLabors.ImageSharp" />
     <PackageReference Include="SixLabors.ImageSharp.Drawing" />
     <PackageReference Include="System.IdentityModel.Tokens.Jwt" />
+    <PackageReference Include="NetCoreServer" />
   </ItemGroup>
 
   <!-- Due to Concentus. -->
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationData.cs b/src/Ryujinx.Ui.Common/App/ApplicationData.cs
index e6130bdac..1be883ee1 100644
--- a/src/Ryujinx.Ui.Common/App/ApplicationData.cs
+++ b/src/Ryujinx.Ui.Common/App/ApplicationData.cs
@@ -65,7 +65,7 @@ namespace Ryujinx.Ui.App.Common
 
             if (extension is ".nsp" or ".xci")
             {
-                PartitionFileSystem pfs;
+                IFileSystem pfs;
 
                 if (extension == ".xci")
                 {
@@ -75,7 +75,9 @@ namespace Ryujinx.Ui.App.Common
                 }
                 else
                 {
-                    pfs = new PartitionFileSystem(file.AsStorage());
+                    var pfsTemp = new PartitionFileSystem();
+                    pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+                    pfs = pfsTemp;
                 }
 
                 foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
index 2bfa7e25d..1ae96b9fc 100644
--- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
@@ -174,7 +174,7 @@ namespace Ryujinx.Ui.App.Common
                         {
                             try
                             {
-                                PartitionFileSystem pfs;
+                                IFileSystem pfs;
 
                                 bool isExeFs = false;
 
@@ -186,7 +186,9 @@ namespace Ryujinx.Ui.App.Common
                                 }
                                 else
                                 {
-                                    pfs = new PartitionFileSystem(file.AsStorage());
+                                    var pfsTemp = new PartitionFileSystem();
+                                    pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+                                    pfs = pfsTemp;
 
                                     // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
                                     bool hasMainNca = false;
@@ -500,7 +502,7 @@ namespace Ryujinx.Ui.App.Common
             ApplicationCountUpdated?.Invoke(null, e);
         }
 
-        private void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId)
+        private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId)
         {
             (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0);
 
@@ -563,7 +565,7 @@ namespace Ryujinx.Ui.App.Common
                     {
                         try
                         {
-                            PartitionFileSystem pfs;
+                            IFileSystem pfs;
 
                             bool isExeFs = false;
 
@@ -575,7 +577,9 @@ namespace Ryujinx.Ui.App.Common
                             }
                             else
                             {
-                                pfs = new PartitionFileSystem(file.AsStorage());
+                                var pfsTemp = new PartitionFileSystem();
+                                pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+                                pfs = pfsTemp;
 
                                 foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
                                 {
@@ -827,7 +831,7 @@ namespace Ryujinx.Ui.App.Common
             return false;
         }
 
-        public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, PartitionFileSystem pfs, int programIndex)
+        public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex)
         {
             Nca mainNca = null;
             Nca patchNca = null;
@@ -936,7 +940,8 @@ namespace Ryujinx.Ui.App.Common
                     if (File.Exists(updatePath))
                     {
                         FileStream file = new(updatePath, FileMode.Open, FileAccess.Read);
-                        PartitionFileSystem nsp = new(file.AsStorage());
+                        PartitionFileSystem nsp = new();
+                        nsp.Initialize(file.AsStorage()).ThrowIfFailure();
 
                         return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
                     }
diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
index 9d2df5f03..9ed8fd8cc 100644
--- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
@@ -571,6 +571,7 @@ namespace Ryujinx.Ui.Common.Configuration
             {
                 LanInterfaceId = new ReactiveObject<string>();
                 Mode = new ReactiveObject<MultiplayerMode>();
+                Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode));
             }
         }
 
diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs
index e20f96d15..7fe2bfe5b 100644
--- a/src/Ryujinx/Ui/MainWindow.cs
+++ b/src/Ryujinx/Ui/MainWindow.cs
@@ -1156,6 +1156,14 @@ namespace Ryujinx.Ui
             Graphics.Gpu.GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE;
         }
 
+        public void UpdateInternetAccess()
+        {
+            if (_gameLoaded)
+            {
+                _emulationContext.Configuration.EnableInternetAccess = ConfigurationState.Instance.System.EnableInternetAccess.Value;
+            }
+        }
+
         public static void SaveConfig()
         {
             ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
index ea60421f8..5af181b08 100644
--- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
+++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
@@ -211,7 +211,7 @@ namespace Ryujinx.Ui.Widgets
                         (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
                         (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
                     {
-                        PartitionFileSystem pfs;
+                        IFileSystem pfs;
 
                         if (System.IO.Path.GetExtension(_titleFilePath) == ".xci")
                         {
@@ -221,7 +221,9 @@ namespace Ryujinx.Ui.Widgets
                         }
                         else
                         {
-                            pfs = new PartitionFileSystem(file.AsStorage());
+                            var pfsTemp = new PartitionFileSystem();
+                            pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
+                            pfs = pfsTemp;
                         }
 
                         foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs
index 74aef00f4..9f7179467 100644
--- a/src/Ryujinx/Ui/Windows/DlcWindow.cs
+++ b/src/Ryujinx/Ui/Windows/DlcWindow.cs
@@ -88,7 +88,8 @@ namespace Ryujinx.Ui.Windows
 
                     using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
 
-                    PartitionFileSystem pfs = new(containerFile.AsStorage());
+                    PartitionFileSystem pfs = new();
+                    pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
 
                     _virtualFileSystem.ImportTickets(pfs);
 
@@ -153,7 +154,8 @@ namespace Ryujinx.Ui.Windows
 
                     using FileStream containerFile = File.OpenRead(containerPath);
 
-                    PartitionFileSystem pfs = new(containerFile.AsStorage());
+                    PartitionFileSystem pfs = new();
+                    pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
                     bool containsDlc = false;
 
                     _virtualFileSystem.ImportTickets(pfs);
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
index f5186d5c1..dabef14dd 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.cs
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
@@ -671,6 +671,8 @@ namespace Ryujinx.Ui.Windows
             }
 
             ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
+
+            _parent.UpdateInternetAccess();
             MainWindow.UpdateGraphicsConfig();
             ThemeHelper.ApplyTheme();
         }
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
index fcc8c1d19..f0dbd6b63 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.glade
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
@@ -2993,6 +2993,7 @@
                                         <property name="active-id">Disabled</property>
                                         <items>
                                           <item id="Disabled" translatable="yes">Disabled</item>
+                                          <item id="LdnMitm" translatable="yes">ldn_mitm</item>
                                         </items>
                                       </object>
                                       <packing>
@@ -3064,7 +3065,7 @@
                                       <object class="GtkLabel">
                                         <property name="visible">True</property>
                                         <property name="can-focus">False</property>
-                                        <property name="tooltip-text" translatable="yes">The network interface used for LAN features</property>
+                                        <property name="tooltip-text" translatable="yes">The network interface used for LAN/LDN features</property>
                                         <property name="halign">end</property>
                                         <property name="label" translatable="yes">Network Interface:</property>
                                       </object>
@@ -3079,7 +3080,7 @@
                                       <object class="GtkComboBoxText" id="_multiLanSelect">
                                         <property name="visible">True</property>
                                         <property name="can-focus">False</property>
-                                        <property name="tooltip-text" translatable="yes">The network interface used for LAN features</property>
+                                        <property name="tooltip-text" translatable="yes">The network interface used for LAN/LDN features</property>
                                         <property name="active-id">0</property>
                                         <items>
                                           <item id="0" translatable="yes">Default</item>
diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs
index 6ea4fcf66..2c06c1e7a 100644
--- a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs
+++ b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs
@@ -96,7 +96,8 @@ namespace Ryujinx.Ui.Windows
             {
                 using FileStream file = new(path, FileMode.Open, FileAccess.Read);
 
-                PartitionFileSystem nsp = new(file.AsStorage());
+                PartitionFileSystem nsp = new();
+                nsp.Initialize(file.AsStorage()).ThrowIfFailure();
 
                 try
                 {