ModernUO/dev-docs/networking-packets.md
Kamron Batman 927c87702d
feat(network): allowlist false-positive IPs, escalate on behaviour
The shard owner, on a Starlink CGNAT address, was blocked by the imported
reputation blocklist. The cause was not CrowdSec: the address was a literal line
in ip-blocklist.txt, so BlocklistFilter denied it at accept and then promoted it.
Clearing the CrowdSec decision could not fix it either, because the file entry
re-reports within promoteSuppression of every reconnect.

Reputation feeds list shared consumer address space constantly. On CGNAT one
public address fronts many subscribers at the same time, so a single abusive
customer gets the address listed and everyone else behind it is blocked with
them; where leases rotate, a listing says little about whoever holds the address
now. So exemptions go where they cost nothing, and escalation is driven by what a
connection actually does.

GENERATOR (tools/Export-IpBlocklist.ps1)

- -AllowlistFile takes multiple paths, subtracted from the merged set before the
  output is written. Defaults to the operator's own ip-allowlist.txt (created
  once, never rewritten) plus one generated file per network carve-out.
- Subtraction is range-correct: an allowlisted address inside a blocked CIDR
  splits that CIDR around the hole instead of being ignored. This also fixes
  -ExcludeAnonymizers, which parsed CIDR entries and then only subtracted
  singles.
- Carve-outs are a table (name, ASN, reason, offline seed) rather than a
  hardcoded network, so covering another CGNAT provider is one row.
  -RefreshCarveouts re-fetches from the ASN's current routing announcements and
  collapses them; -Carveout '' subtracts none. Announcements rather than
  ownership records, because registry data disagrees with what is routed and caps
  its result sets.
- Editing an allowlist bypasses -MinInterval, so a just-added exemption is not
  indistinguishable from the allowlist not working.
- The shipped starlink carve-out costs ~0.1% of the list.

ALLOWLISTS

- FileAllowlist reads the same files the generator subtracts, so an operator
  entry means "leave this address alone" for real. Subtraction alone only covers
  being BLOCKED; behavioural detections never consult the blocklist, so without
  this a carve-out was quietly routed around and one scanner behind a shared
  address was enough to get everyone behind it firewalled. Reading the files also
  means an entry applies on the next reload rather than the next regeneration.
- LoginAllowlist is earned by authenticating, with a 90 day TTL because an
  address that logged in years ago is a stranger. Its own store rather than
  Account.LoginIPs, which has no timestamps and cannot be backfilled. An entry is
  evidence rather than a licence: 10 suppressed contributions in an hour revokes
  it, and a fresh login forgives the tally.
- Both are consulted only AFTER the blocklist has already matched, so a normal
  accept pays nothing for them and the accept gate stays allowlist-free.
- BanExemptions combines them behind BanChannel.IsExempt. Suppresses escalation
  only; every local defence still applies.

BEHAVIOURAL DETECTION

- silent-connect (reaped having sent zero bytes) and invalid-seed (a zero seed)
  are contributed. Both were already disconnected.
- ForeignProtocol positively identifies HTTP, TLS and SSH. Asking "is this a good
  UO client?" cannot work: LoginEncryption.ClientDecrypt is a byte-for-byte
  stream XOR, so a client with encryption on when the shard expects none sends a
  structurally perfect connection whose payload is noise. Nothing assumes arrival
  framing, since TCP has no message boundaries and a rule of the form "these
  bytes must arrive together" drops real players on poor links.
- Keyed on bytes-received rather than elapsed time throughout. A connection that
  sent something and ran out of time is far more likely a slow link, and banning
  those makes the player retry, trip the rate limiter, and compound it.
- AutoDenylist holds behavioural detections locally for 15 minutes, as an
  IConnectionFilter plus IBanReporter over one store so engine detection sites
  never reach into content. Closes the gap where a flood pays for a socket and a
  NetState per connection while waiting for an OS bouncer, and is the whole
  defence on a shard running none. Not persisted: a holding pen that survives
  restarts is a ban without a ban's review.
- BanReasons centralises the slugs. IsBehavioral is an opt-in set, not
  "everything except manual", so a future reason escalates normally instead of
  silently inheriting an exemption. The first cut of the exemption swallowed
  manual admin bans; this is why.

FIXES

- BanConfiguration.Settings was null until Configure() ran while the reap path
  dereferences it every Slice(), so a harness driving NetState.Slice() directly
  hit an NRE that looked flaky because it depended on test ordering.
- -AllowlistFile was typed [string] while documented and used as a list.

LAYOUT AND DOCS

Content network code moves out of Misc into UOContent/Network, one concern per
folder. Namespaces are untouched, so these are pure file moves.
dev-docs/ip-bans-and-allowlists.md documents the subsystem, leading with the
operator process for unblocking a player -- including the three things that look
sufficient and are not: deleting the CrowdSec decision alone, editing
ip-blocklist.txt by hand, and cscli allowlists alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:31:34 -07:00

22 KiB

ModernUO Networking & Packets

This document covers ModernUO's networking system, including outgoing and incoming packet patterns, SpanWriter/SpanReader, and NetState extensions.

Overview

ModernUO uses a binary packet protocol for client-server communication. The system is built around:

  • Outgoing packets: Static Create* methods + Send* extension methods on NetState
  • Incoming packets: Function pointer handlers registered in Configure()
  • SpanWriter/SpanReader: High-performance binary I/O using Span<byte>

Outgoing Packet Pattern

Step 1: Define Constants and Create Method

public static class OutgoingMyPackets
{
    public const int MyPacketLength = 12;  // Fixed-size packet

    public static void CreateMyPacket(Span<byte> buffer, Serial target, int value)
    {
        if (buffer[0] != 0)  // Already initialized guard
            return;

        var writer = new SpanWriter(buffer);
        writer.Write((byte)0xBF);       // Packet ID
        writer.Write((ushort)12);       // Packet length
        writer.Write((ushort)0x99);     // Sub-command
        writer.Write(target);           // Serial (4 bytes)
        writer.Write((short)value);     // Value (2 bytes)
    }
}

Step 2: Define Send Extension Method

public static void SendMyPacket(this NetState ns, Serial target, int value)
{
    if (ns.CannotSendPackets())
        return;

    var buffer = stackalloc byte[MyPacketLength].InitializePacket();
    CreateMyPacket(buffer, target, value);
    ns.Send(buffer);
}

Step 3: Call from Game Code

// Send to one player
mobile.NetState.SendMyPacket(target.Serial, 42);

// Send to nearby players
foreach (var ns in mobile.GetClientsInRange(18))
{
    ns.SendMyPacket(target.Serial, 42);
}

Variable-Length Outgoing Packets

public static void SendMyDynamicPacket(this NetState ns, string name, int[] values)
{
    if (ns.CannotSendPackets())
        return;

    var length = 7 + name.Length * 2 + values.Length * 4;
    var writer = new SpanWriter(stackalloc byte[length]);

    writer.Write((byte)0x99);          // Packet ID
    writer.Write((ushort)0);           // Length placeholder
    writer.WriteBigUniNull(name);      // Unicode string
    writer.Write((ushort)values.Length);

    foreach (var val in values)
        writer.Write(val);

    writer.WritePacketLength();        // Fill in actual length at position 1-2
    ns.Send(writer.Span);
}

Shared Buffer Pattern (Multiple Recipients)

When sending the same packet to multiple players, create the buffer once:

public static void SendToNearby(Mobile source, int effectId)
{
    Span<byte> buffer = stackalloc byte[EffectPacketLength];
    buffer.InitializePacket();

    foreach (var ns in source.GetClientsInRange(18))
    {
        // CreateXxx checks buffer[0] != 0 to avoid re-initializing
        CreateEffectPacket(buffer, source.Serial, effectId);
        ns.Send(buffer);
    }
}

Incoming Packet Pattern

Step 1: Register Handler in Configure()

public static class IncomingMyPackets
{
    public static unsafe void Configure()
    {
        // Fixed-length packet (12 bytes, in-game only)
        IncomingPackets.Register(0x99, 12, true, &MyHandler);

        // Variable-length packet (0 = variable)
        IncomingPackets.Register(0x9A, 0, true, &MyDynamicHandler);

        // Out-of-game packet
        IncomingPackets.Register(0x9B, 10, false, &LoginHandler);

        // Encoded packet (sub-command)
        IncomingPackets.RegisterEncoded(0x28, true, &EncodedHandler);
    }
}

Step 2: Implement Handler

public static void MyHandler(NetState state, SpanReader reader)
{
    var from = state.Mobile;
    if (from == null)
        return;

    var targetSerial = (Serial)reader.ReadUInt32();
    var value = reader.ReadInt16();

    var target = World.FindMobile(targetSerial);
    if (target == null)
        return;

    // Process packet...
}

public static void MyDynamicHandler(NetState state, SpanReader reader)
{
    var from = state.Mobile;
    if (from == null)
        return;

    var name = reader.ReadBigUniSafe();
    var count = reader.ReadUInt16();

    for (var i = 0; i < count; i++)
    {
        var val = reader.ReadInt32();
        // Process each value...
    }
}

Encoded Packet Handler

public static void EncodedHandler(NetState state, IEntity target, EncodedReader reader)
{
    // Encoded packets have a different signature
    var from = state.Mobile;
    if (from == null)
        return;

    // Process...
}

Registration Parameters

IncomingPackets.Register(
    int packetID,       // Packet identifier (0x00-0xFF)
    int length,         // Fixed length, or 0 for variable-length
    bool inGameOnly,    // Requires authenticated player
    delegate*<NetState, SpanReader, void> handler  // Function pointer
);

SpanWriter Reference

High-performance ref struct for writing binary data. Defined in Projects/Server/Buffers/SpanWriter.cs.

Constructors

var writer = new SpanWriter(Span<byte> buffer);              // Fixed buffer
var writer = new SpanWriter(stackalloc byte[64]);             // Stack buffer
var writer = new SpanWriter(int capacity, bool resize = false); // Pooled buffer

Integer Writes (Big-Endian by Default)

writer.Write(bool value);        // 1 byte
writer.Write(byte value);        // 1 byte
writer.Write(sbyte value);       // 1 byte
writer.Write(short value);       // 2 bytes, big-endian
writer.Write(ushort value);      // 2 bytes, big-endian
writer.Write(int value);         // 4 bytes, big-endian
writer.Write(uint value);        // 4 bytes, big-endian
writer.Write(long value);        // 8 bytes, big-endian
writer.Write(ulong value);       // 8 bytes, big-endian
writer.Write(Serial serial);     // 4 bytes (writes serial.Value)

Little-Endian Variants

writer.WriteLE(short value);
writer.WriteLE(ushort value);
writer.WriteLE(int value);
writer.WriteLE(uint value);

String Writes

// ASCII (1 byte per char)
writer.WriteAscii(string value);
writer.WriteAsciiNull(string value);       // Null-terminated
writer.WriteAscii(string value, int fixedLength);

// Latin-1 (1 byte per char, extended ASCII)
writer.WriteLatin1(string value);
writer.WriteLatin1Null(string value);
writer.WriteLatin1(string value, int fixedLength);

// UTF-16 Big-Endian (UO standard for Unicode)
writer.WriteBigUni(string value);
writer.WriteBigUniNull(string value);
writer.WriteBigUni(string value, int fixedLength);

// UTF-16 Little-Endian
writer.WriteLittleUni(string value);
writer.WriteLittleUniNull(string value);
writer.WriteLittleUni(string value, int fixedLength);

// UTF-8
writer.WriteUTF8(string value);
writer.WriteUTF8Null(string value);

Utilities

writer.Write(ReadOnlySpan<byte> data);    // Raw bytes
writer.Clear(int count);                   // Write zeros
writer.Seek(int offset, SeekOrigin origin); // Move position
writer.WritePacketLength();                // Fill length at position 1-2
writer.EnsureCapacity(int capacity);       // Grow buffer if needed
writer.Dispose();                          // Return pooled buffer

// Properties
writer.Position;     // Current write position
writer.Capacity;     // Buffer size
writer.Span;         // ReadOnlySpan<byte> of written data
writer.RawBuffer;    // Mutable Span<byte> of full buffer

SpanReader Reference

High-performance ref struct for reading binary data. Defined in Projects/Server/Buffers/SpanReader.cs.

Constructor

var reader = new SpanReader(ReadOnlySpan<byte> data);

Integer Reads (Big-Endian by Default)

reader.ReadByte();        // 1 byte
reader.ReadBoolean();     // 1 byte (> 0 = true)
reader.ReadSByte();       // 1 byte signed
reader.ReadInt16();       // 2 bytes, big-endian
reader.ReadUInt16();      // 2 bytes, big-endian
reader.ReadInt32();       // 4 bytes, big-endian
reader.ReadUInt32();      // 4 bytes, big-endian
reader.ReadInt64();       // 8 bytes, big-endian
reader.ReadUInt64();      // 8 bytes, big-endian

Little-Endian Variants

reader.ReadInt16LE();
reader.ReadUInt16LE();
reader.ReadUInt32LE();

String Reads

// Each has a "Safe" variant that filters control characters
reader.ReadAscii(int fixedLength = -1);
reader.ReadAsciiSafe(int fixedLength = -1);
reader.ReadLatin1(int fixedLength = -1);
reader.ReadLatin1Safe(int fixedLength = -1);
reader.ReadBigUni(int fixedLength = -1);
reader.ReadBigUniSafe(int fixedLength = -1);
reader.ReadLittleUni(int fixedLength = -1);
reader.ReadLittleUniSafe(int fixedLength = -1);
reader.ReadUTF8(int fixedLength = -1);
reader.ReadUTF8Safe(int fixedLength = -1);

Utilities

reader.Seek(int offset, SeekOrigin origin);
reader.Read(Span<byte> destination);

// Properties
reader.Position;    // Current read position
reader.Length;      // Total data length
reader.Remaining;   // Bytes remaining
reader.Buffer;      // ReadOnlySpan<byte> of full data

Player-Facing Message APIs

For chat, system messages, and overhead text, prefer the high-level convenience methods on Mobile and Item — they handle stackalloc sizing, packet buffer initialization, spatial queries, and visibility filtering for you. The underlying packets all live in OutgoingMessagePackets.

On Mobile

// Self-message (sent only to this mobile's NetState)
mob.SendMessage(int hue, ReadOnlySpan<char> text);
mob.SendAsciiMessage(int hue, ReadOnlySpan<char> text);
mob.SendLocalizedMessage(int number, ReadOnlySpan<char> args = default, int hue = 0x3B2);
mob.SendLocalizedMessage(int number, bool append, ReadOnlySpan<char> affix, ReadOnlySpan<char> args = default, int hue = 0x3B2);

// Speech variants (overhead text from this mobile, broadcast in range)
mob.Say(ReadOnlySpan<char> text);              // SpeechHue
mob.Say(int number, ReadOnlySpan<char> args = default);
mob.Emote(ReadOnlySpan<char> text);            // EmoteHue
mob.Whisper(ReadOnlySpan<char> text);          // WhisperHue, short range
mob.Yell(ReadOnlySpan<char> text);             // YellHue, long range

// Targeted overhead messages
mob.PublicOverheadMessage(MessageType type, int hue, bool ascii, ReadOnlySpan<char> text, bool noLineOfSight = true, AccessLevel accessLevel = AccessLevel.Player);
mob.PublicOverheadMessage(MessageType type, int hue, int number, ReadOnlySpan<char> args = default, bool noLineOfSight = true);
mob.PrivateOverheadMessage(MessageType type, int hue, int number, ReadOnlySpan<char> args, NetState state);
mob.LocalOverheadMessage(MessageType type, int hue, bool ascii, ReadOnlySpan<char> text);
mob.NonlocalOverheadMessage(MessageType type, int hue, int number, ReadOnlySpan<char> args = default);

On Item

item.PublicOverheadMessage(MessageType type, int hue, bool ascii, ReadOnlySpan<char> text);
item.PublicOverheadMessage(MessageType type, int hue, int number, ReadOnlySpan<char> args = default);
item.SendLocalizedMessageTo(Mobile to, int number, ReadOnlySpan<char> args = default);
item.SendLocalizedMessageTo(Mobile to, int number, int hue, ReadOnlySpan<char> args = default);
item.SendMessageTo(Mobile to, ReadOnlySpan<char> text, int hue = 0x3B2);

Direct NetState extensions

When you have a NetState and need full control (custom serial, body, font, language):

ns.SendMessage(Serial serial, int graphic, MessageType type, int hue, int font, bool ascii, string lang, ReadOnlySpan<char> name, ReadOnlySpan<char> text);
ns.SendMessageLocalized(Serial serial, int graphic, MessageType type, int hue, int font, int number, ReadOnlySpan<char> name = default, ReadOnlySpan<char> args = default);
ns.SendMessageLocalizedAffix(Serial serial, int graphic, MessageType type, int hue, int font, int number, ReadOnlySpan<char> name, AffixType affixType, ReadOnlySpan<char> affix = default, ReadOnlySpan<char> args = default);

Zero-allocation interpolation overloads

Every method above has a ref RawInterpolatedStringHandler overload for the text/args parameter. When the call-site argument is a $"..." literal, the compiler picks the handler overload and the message text is rendered directly into a pooled char[] — no string allocation:

mob.SendMessage($"You have {gold:N0} gold and {bounty:N0} bounty");
mob.Say($"Hello, {target.Name}!");
item.SendLocalizedMessageTo(player, cliloc, $"{a}\t{b}");
mob.PublicOverheadMessage(MessageType.Regular, hue, false, $"I am {mob.Name}");

When the argument is a pre-built string or ReadOnlySpan<char> variable, the ROS<char> overload is selected via implicit conversion — also fine, just doesn't get the zero-alloc benefit.

For methods with two text parameters (SendLocalizedMessageTo with affix, SendLocalizedMessage with append), only args has a handler overload — affix stays ROS<char> because it's typically a short literal.

Critical caveat: call-site shapes like ternaries, switch expressions, pre-built locals, and .ToString() inside the hole silently defeat the handler overload selection. See the Interpolation Anti-Patterns section in the string-handling doc for the full list and the fixes.

Lowercase format specifier

RawInterpolatedStringHandler recognizes :L to lowercase a value's output (using MemoryExtensions.ToLowerInvariant). Useful for enum names in player-facing text:

mob.SendMessage($"You earned a {rank:L} trophy!");          // "gold" not "Gold"

See dev-docs/string-handling.md for full coverage.

Implementation notes

  • Mobile.PublicOverheadMessage and Item.PublicOverheadMessage route through generic OutgoingMessagePackets.BroadcastMessage*<TFilter> helpers (in OutgoingMessagePackets.Broadcast.cs) parameterized over a private readonly struct filter that encapsulates the per-method visibility predicate (CanSee, InLOS, AccessLevel, != self). The where TFilter : struct, IBroadcastFilter constraint specializes per filter type and keeps the dispatch zero-allocation (no boxing, no virtual call — JIT inlines the predicate).
  • The convenience methods themselves live in Mobile.Messages.cs and Item.Messages.cs partial files for organization.

Common Existing Send Methods

Effects and Sounds

ns.SendSoundEffect(int soundID, IPoint3D target);
ns.SendMobileAnimation(Serial mobile, int action, int frames, int repeat, bool forward, bool loop, int delay);
ns.SendNewMobileAnimation(Serial mobile, int action, int frames, int delay);

Mobile Status

ns.SendMobileHits(Mobile m, bool normalize = false);
ns.SendMobileMana(Mobile m, bool normalize = false);
ns.SendMobileStam(Mobile m, bool normalize = false);
ns.SendMobileAttributes(Mobile m, bool normalize = false);
ns.SendMobileStatus(Mobile m);
ns.SendMobileName(Mobile m);
ns.SendMobileMoving(Mobile source, Mobile target);
ns.SendBondedStatus(Serial serial, bool bonded);
ns.SendDeathAnimation(Serial killed, Serial corpse);

Damage

ns.SendDamage(Serial serial, int amount);

Targeting

ns.SendTargetReq(Target target);
ns.SendMovementRej(int sequence, Mobile m);

Protocol Notes

  • Endianness: UO protocol is big-endian by default
  • Packet ID: First byte identifies the packet type (0x00-0xFF)
  • Length: For variable-length packets, bytes 1-2 are the total length (big-endian ushort)
  • Serials: 4-byte identifiers for items (0x40000000+) and mobiles (0x00000001+)
  • Clilocs: 4-byte localized string IDs

Best Practices

  1. Always check ns.CannotSendPackets() before sending
  2. Use stackalloc for fixed-size packets (avoids heap allocation)
  3. Use InitializePacket() extension on stackalloc spans
  4. Use ReadAsciiSafe/ReadBigUniSafe for incoming strings (filters control chars)
  5. Use WritePacketLength() for variable-length packets
  6. Big-endian by default -- only use WriteLE/ReadLE when the protocol requires it
  7. Function pointers (&Handler) for incoming packet registration (no delegate allocation)

Connection Filtering (Accept Path)

Every inbound socket is checked before it becomes a NetState. The check runs on the game loop once per accepted connection -- this is the path that has to survive a DDoS -- so it must be allocation-free and non-blocking.

Gates plug in through IConnectionFilter, registered with ConnectionFilters.Register() during the Configure sweep:

public sealed class MyFilter : IConnectionFilter
{
    public string Name => "my-filter";
    public void Configure() { /* read config, no I/O */ }
    public void Start(CancellationToken token) { /* background hydration */ }
    public void Stop() { }
    public bool ShouldDeny(IPAddress address) => /* allocation-free membership test */;
}

// In a static Configure() so the sweep finds it:
ConnectionFilters.Register(new MyFilter());

Rules:

  • ShouldDeny must be allocation-free, O(log n) at worst, no I/O, no blocking. Anything expensive (parsing, reloading, contributing to an external service) belongs off the loop or behind a bounded, non-blocking enqueue.
  • Side effects a hit implies (reporting to BanChannel, promoting to an OS firewall, suppressing duplicate reports) are the filter's business, not the accept path's.
  • Filters are consulted in registration order and the first denial short-circuits, so register the cheapest and most selective first. Order affects only how quickly a denial is reached, never whether one happens.
  • A filter that throws is unregistered and the connection fails open. A filter that faults once faults for every connection, so leaving it registered would mean an exception per accept.

Core owns the question; every implementation lives in UOContent. The three that ship are firewall (admin-curated, mutable at runtime, persisted to Configuration/firewall.json), blocklist (file-sourced, millions of entries, demand-pages hits to CrowdSec) and auto-denylist (in-memory, short-lived, fed by the shard's own behavioural detections). A shard that fronts its server with an upstream proxy or edge scrubbing can drop all of them and register nothing.

The allowlists, ban contribution, behavioural detection and the operator process for exempting a false-positive address are covered separately in dev-docs/ip-bans-and-allowlists.md.

Do not route this kind of check through EventSink.InvokeSocketConnect -- that fires later and allocates a SocketConnectEventArgs per connection, which is exactly what the accept path avoids for rejected traffic.

IP Address Normalization (IPAddressUtility)

Addresses are normalized to UInt128 in IPv6 form so a single comparison/index works for both families. An IPv4 address becomes its v4-mapped-v6 value (::ffff:a.b.c.d), which is why round-tripping matters: IPv4 -> UInt128 -> IPAddress can come back as InterNetworkV6 with IsIPv4MappedToIPv6 set, even though it is "really" a v4 address. Code that switches on AddressFamily alone will mis-handle those, so the helpers check both.

Known wart / follow-up: ToUInt128 guards with AddressFamily == InterNetwork && !IsIPv4MappedToIPv6. Per BCL semantics IsIPv4MappedToIPv6 is only ever true for InterNetworkV6, so the second clause reads as redundant -- it is really defending the round-trip described above. The normalization would be clearer as an explicit "to canonical v6 bits" step that never needs the family check at all. Deliberately left as-is; to be revisited in a follow-up PR rather than churned mid-feature.

Key File References

File Description
Projects/Server/Buffers/SpanWriter.cs SpanWriter ref struct
Projects/Server/Buffers/SpanReader.cs SpanReader ref struct
Projects/Server/Network/Packets/IncomingPackets.cs Packet registration
Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs Player packet handlers
Projects/UOContent/Network/Packets/IncomingMovementPackets.cs Movement handlers
Projects/UOContent/Network/Packets/IncomingMessagePackets.cs Speech handlers
Projects/UOContent/Network/Packets/IncomingItemPackets.cs Item handlers
Projects/UOContent/Network/Packets/IncomingTargetingPackets.cs Targeting handlers
Projects/Server/Network/Packets/OutgoingMobilePackets.cs Mobile packets
Projects/Server/Network/Packets/OutgoingItemPackets.cs Item packets
Projects/Server/Network/Packets/OutgoingDamagePackets.cs Damage packets
Projects/Server/Network/Packets/OutgoingEffectPackets.cs Effect/sound packets
Projects/Server/Network/Packets/OutgoingAccountPackets.cs Account packets
Projects/Server/Network/Packets/OutgoingContainerPackets.cs Container packets
Projects/Server/Network/PacketHandler.cs PacketHandler class
Projects/Server/Network/IConnectionFilter.cs Accept-path gate contract
Projects/Server/Network/ConnectionFilters.cs Filter registry + lifecycle
Projects/UOContent/Network/Firewall/Firewall.cs Admin-curated firewall set
Projects/Server/Utilities/IPAddressUtility.cs IPAddress <-> UInt128 normalization, CIDR parsing
Projects/UOContent/Network/Blocklist/BlocklistFilter.cs File-sourced blocklist filter
Projects/UOContent/Network/LoginAllowlist/LoginAllowlist.cs Allowlist earned by a recent successful login
Projects/UOContent/Network/AutoDenylist/AutoDenylist.cs Short-lived local hold on behavioural detections
Projects/Server/Network/Bans/BanReasons.cs Ban reason slugs + the behavioural opt-in set
Projects/Server/Network/ForeignProtocol.cs Positive identification of non-UO traffic (HTTP/TLS/SSH)