Obfusquer un service gRPC sans casser le contrat sur le fil
gRPC met deux sortes de noms dans votre assembly, et l'obfuscation doit les traiter comme des opposés. Les noms qui voyagent sur le fil — le chemin du service, le chemin de la méthode, les numéros de champ d'un message protobuf — sont un contrat dont dépend chaque pair, et les renommer casse l'interopérabilité aussi sûrement que changer une URL. Les noms qui ne quittent jamais le processus — vos noms de méthode C#, les noms de propriété de vos messages, vos enregistrements d'injection de dépendances — sont libres de disparaître. Le piège est que la sorte d'un nom donné dépend entièrement de si vous êtes parti contract-first avec protoc ou code-first avec protobuf-net.Grpc, car code-first dérive l'identité du fil de vos noms .NET par réflexion, transformant les noms libres en noms porteurs. Ce billet lit le code généré et les métadonnées pour montrer exactement où vit l'identité du fil dans chaque style, pourquoi les numéros de champ protobuf rendent les membres de message sûrs à renommer, et comment Nebula épingle la surface du contrat tout en renommant tout ce qui se trouve derrière.
Un assembly gRPC est plein de noms, et l”obfuscation doit les trier en deux tas dont les règles sont exactement opposées.
Dans un tas se trouvent les noms qui voyagent entre processus : le chemin de service qu”un client compose, le chemin de méthode à l”intérieur, les numéros de champ qui identifient chaque champ d”un message sérialisé. Ceux-là sont un contrat. Chaque autre pair — un service écrit en Go, un client mobile, la build de l”an dernier de votre propre passerelle — dépend de leur stabilité, et en renommer un est indiscernable d”un changement d”URL publiée. Dans l”autre tas se trouvent les noms qui ne quittent jamais le processus : vos noms de méthode C#, les propriétés de vos classes de message, le type que vous passez à MapGrpcService<T>. Ce sont des étiquettes que le CLR résout en interne, et elles peuvent être aplaties en a, b, c sans effet observable.
Toute la question d”obfusquer gRPC en sécurité est quel nom est dans quel tas — et la réponse bascule complètement selon que vous avez généré votre service depuis un fichier .proto ou l”avez défini en C# avec protobuf-net.Grpc.
Contract-first : l”identité du fil est figée dans le code généré
Commençons par le cas courant — un .proto compilé par Grpc.Tools. Le contrat est déclaré une fois, dans un fichier qui n”est même pas du C# :
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string greeting = 1;
}
Le générateur en fait un Greeter.g.cs dont le travail est de porter l”identité du fil sous forme de littéraux. La surface de routage ressemble à ceci (abrégée à l”essentiel) :
public static partial class Greeter
{
static readonly string __ServiceName = "greet.Greeter";
static readonly grpc::Method<HelloRequest, HelloReply> __Method_SayHello =
new grpc::Method<HelloRequest, HelloReply>(
grpc::MethodType.Unary,
__ServiceName, // "greet.Greeter"
"SayHello", // method name — a literal, not derived
__Marshaller_HelloRequest,
__Marshaller_HelloReply);
// ...
}
Lisez ce que cela signifie pour le renommage. Le chemin qu”un client compose est /greet.Greeter/SayHello, et il est assemblé à partir des littéraux de chaîne "greet.Greeter" et "SayHello" posés dans le descripteur Method. Il n”est pas dérivé du nom d”une quelconque méthode C# à l”exécution. Donc votre implémentation réelle :
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext ctx) =>
Task.FromResult(new HelloReply { Greeting = $"Hello {request.Name}" });
peut voir son identifiant C# renommé en b et le répartiteur le liera toujours au littéral "SayHello" du descripteur, car le constructeur de la définition de service du serveur fait correspondre le nom de méthode du descripteur au délégué — l”identifiant de la méthode est sans importance pour le routage. Les noms que vous devez conserver sont ceux à l”intérieur du fichier généré : __ServiceName et les descripteurs Method, car ces littéraux sont le contrat du fil.
Les numéros de champ rendent libres les membres de message
Les messages sont l”autre moitié, et ici protobuf vous rend service. Le HelloRequest généré sérialise par numéro, pas par nom :
public void WriteTo(pb::CodedOutputStream output)
{
if (Name.Length != 0)
{
output.WriteRawTag(10); // (field 1 << 3) | wiretype 2 == the tag for "name"
output.WriteString(Name);
}
}
Le tag 10 encode le numéro de champ 1 avec le type de fil 2 (délimité par longueur). Ce tag est ce que lit un pair ; la propriété C# Name n”est qu”un accesseur local. Renommez Name en a et WriteTo émet toujours le tag 10, donc chaque pair qui lit le champ 1 n”est pas affecté. Les noms de propriété de message sont fermement dans le tas des libres-à-renommer : le numéro de champ porte l”identité, et le numéro vit dans le .proto, pas dans vos identifiants.
Deux réserves. Premièrement, si vous exposez le transcodage gRPC-JSON ou une passerelle JSON, cette surface se fonde sur le nom de champ en camelCase, donc les noms de champ deviennent visibles sur le fil et doivent être préservés sur les messages transcodés. Deuxièmement, les noms de valeur d”énumération sont sérialisés comme des chaînes par le formateur JSON et sont visibles via la réflexion serveur, donc si vous dépendez de l”un ou l”autre, épinglez les noms de membre d”énumération. Le gRPC binaire pur ne dépend d”aucun.
Le code-first inverse les tas : vos noms .NET sont le contrat
Maintenant le cas qui fait trébucher. Avec protobuf-net.Grpc il n”y a pas de .proto ni de descripteur généré plein de littéraux. Vous écrivez le contrat en C# :
[ServiceContract]
public interface IGreeter
{
[OperationContract]
Task<HelloReply> SayHelloAsync(HelloRequest request, CallContext ctx = default);
}
[ProtoContract]
public class HelloRequest
{
[ProtoMember(1)] public string Name { get; set; } = "";
}
Au démarrage, la bibliothèque réfléchit sur IGreeter et dérive l”identité du fil des noms .NET : le chemin de service vient du nom de l”interface (IGreeter → service Greeter par sa convention de nommage par défaut), et chaque chemin de méthode vient du nom de méthode (SayHelloAsync). Il n”y a pas de littéraux à préserver car les noms eux-mêmes sont les littéraux. Renommez IGreeter en a et vous avez renommé le chemin de service ; renommez SayHelloAsync en b et vous avez déplacé la méthode. Une passe de renommage aveugle change silencieusement le contrat et chaque client échoue à trouver le point de terminaison qu”il appelait.
Les messages restent sûrs — [ProtoMember(1)] épingle le numéro de champ 1 quel que soit le nom de la propriété Name, exactement comme en contract-first — mais la surface du service s”est déplacée dans le tas à préserver. Deux façons de le gérer, par ordre de préférence :
Le correctif robuste est de cesser de dépendre des identifiants en donnant au contrat des noms explicites :
[ServiceContract(Name = "greet.Greeter")]
public interface IGreeter
{
[OperationContract(Name = "SayHello")]
Task<HelloReply> SayHelloAsync(HelloRequest request, CallContext ctx = default);
}
Maintenant l”identité du fil est "greet.Greeter"/"SayHello" depuis les attributs, les identifiants IGreeter et SayHelloAsync sont de nouveau libres à renommer, et votre contrat ne dépend plus du nom de votre code — ce qui est une bonne hygiène même si vous n”obfusquez jamais. L”alternative, quand vous ne pouvez pas éditer les contrats, est d”épingler les noms de type et de membre des interfaces de contrat pour que la réflexion lise toujours les noms qu”elle attend.
Dire à Nebula quel tas est lequel
Nebula ne devine pas. Pour le contract-first, il reconnaît la surface du service généré à la forme du descripteur et conserve la constante de nom de service, les descripteurs Method et (quand c”est configuré) les noms de valeur d”énumération, tout en renommant vos implémentations, les identifiants de propriété de message, le câblage de DI et les intercepteurs. Pour le code-first, vous le pointez vers les attributs de contrat pour qu”il traite les types et membres [ServiceContract]/[OperationContract] comme l”ensemble à préserver.
<Nebula>
<!-- Contract-first: keep the wire identity the generator baked in. -->
<!-- Service-name constants and Method<,> descriptors in generated gRPC code. -->
<Preserve names="true" members="true"
attribute="System.CodeDom.Compiler.GeneratedCodeAttribute"
where="GrpcServices" />
<!-- Code-first: the .NET names ARE the contract surface. -->
<Preserve names="true" members="true" attribute="System.ServiceModel.ServiceContractAttribute" />
<Preserve names="true" members="true" attribute="System.ServiceModel.OperationContractAttribute" />
<!-- Enums whose value names hit the wire via JSON/reflection. -->
<Preserve memberNames="true" where="ProtoEnums" />
<!-- Everything else — service implementations, message property names,
interceptors, private helpers — gets full renaming. -->
</Nebula>
L”enregistrement de DI ne demande aucun traitement spécial. builder.Services.AddGrpc() et app.MapGrpcService<GreeterService>() lient par Type, pas par nom, donc GreeterService peut être renommé en d et la correspondance résout toujours — l”argument générique est un jeton de métadonnées, comme toutes les références de type, et le renommage le met à jour de façon cohérente. De même, les marshallers, le CallContext et vos intercepteurs sont de la plomberie interne qui se renomme proprement.
La seule règle qui couvre les deux styles
Ôtez les spécificités et gRPC obéit à la même règle que tout autre framework qui franchit une frontière : un nom est porteur exactement quand quelque chose hors de la résolution de jetons du CLR le lit. Sur le fil binaire, c”est le chemin de service, le chemin de méthode et les numéros de champ — et les numéros de champ veulent dire que vos membres de message sont libres. Via le transcodage JSON ou la réflexion, cela veut dire en plus les noms de champ et les noms de valeur d”énumération. Et en code-first, la réflexion au démarrage tire vos noms de type et de membre .NET à travers cette frontière, ce qui est la raison même pour laquelle le code-first change la réponse.
Trouvez cette frontière, préservez précisément les noms qui la franchissent, et laissez Nebula renommer tout ce qui reste de votre côté. Vous gardez la protection sur vos implémentations de service et votre logique métier — le code qui mérite vraiment d”être protégé — et le contrat sur le fil ne bouge jamais.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.