Skip to content

Netstrings למסגור הודעות רשת ומגבלות של מפענח בטוח

Netstrings למסגור הודעות רשת ומגבלות של מפענח בטוח

גרסה לימודית מתוקנת; תאריך המקור האנגלי: 2026-07-16T11:17:57.687+03:00. המהדורה בעברית משתמשת בתאריך הפרסום שלה בפועל, ולא בתאריך המקור באנגלית. כל דוגמאות הקוד והמספרים להמחשה בלבד ולא הורצו או נמדדו במסגרת ההכנה הזאת. English original

שלום לכולם, כאן אלכס מ־Coding with Alex. ברוכים השבים לבלוג.

מסגור זרם וסריאליזציה פותרים בעיות שונות. JSON, ‏XML ו־Protocol Buffers מתארים מטענים, אך ב־TCP עדיין דרושים גבולות בין הודעות. Daniel J. Bernstein תיעד את Netstrings בטיוטת אינטרנט משנת 1997: קידוד קטן עם קידומת אורך שיכול למסגר בתים שרירותיים, לרבות מטען JSON שעבר סריאליזציה. אין בכך הוכחה ליתרון מהירות גורף. [מקור 1]

אני מדבר על Netstrings.

Netstrings נשארים אפשרות שימושית לתכנון פרוטוקולים מותאמים קטנים, לרבות טלמטריה ושירותים פנימיים. המאמר מסביר את תבנית הבתים, פשרות המימוש ומגבלות המשאבים. אין כאן טענה לתחייה שנמדדה, לעליונות על פרוטוקולים מודרניים או להבטחת אבטחה והשהיה. [מקור 1]

הבעיה היא מסגור בעולם של זרמים

לפני שנבחן Netstrings, נדבר על הבעיה הבסיסית שהם פותרים: מסגור.

TCP מציג זרם בתים מסודר, ולא גבולות של הודעות יישום. פעולות כתיבה נפרדות עשויות להתאחד בקריאה אחת או להתפצל בין קריאות; מסגור היישום חייב לטפל בשני המצבים. TCP עצמו כן משתמש במקטעים ברשת. [מקור 1]

כדי לשחזר את ההודעות המקוריות צריך לקבוע היכן הודעה אחת מסתיימת והבאה מתחילה. מפתחים בדרך כלל פותרים זאת באחת משתי דרכים:

  • מסגור באמצעות מפריד מחפש תו מסיים, כגון ירידת שורה או בית NUL. מטענים בינאריים שרירותיים דורשים escaping או קידוד מוסכם אחר; בפורמטים מוגבלים ייתכן שהמסיים כבר אסור במטען. אגירה מוגבלת ופענוח קפדני נחוצים למניעת מיצוי משאבים ואי־בהירות. [מקור 1]
  • מסגור עם קידומת אורך שולח גודל ואחריו מטען. קידומת בינארית ברוחב קבוע דורשת סדר בתים מוסכם, לא תלות בארכיטקטורה של אחד המעבדים. קידומת עשרונית ב־ASCII קלה יותר לבדיקה ידנית, אך גם בה נחוצות מגבלות ובדיקת ספרות קפדנית. [מקור 1]

Netstrings משלבים קידומת אורך עשרונית ופסיק מסיים. הקידומת קריאה; המטען עשוי להיות בינארי. התכנון מאפשר בדיקת גודל מוקדמת, אך אינו מספק לבדו אבטחה ליישום. [מקור 1]

מהו Netstring

טיוטת האינטרנט הארכיונית של Bernstein מגדירה את הקידוד הבא למחרוזת בתים. זו הצעה היסטורית, לא RFC של IETF ולא תקן אינטרנט. [מקור 1]

[len]:[string],

כך מפרקים את התבנית:

  • [len]: אורך המטען, כמחרוזת ASCII של ספרות עשרוניות. אסור להשתמש באפסים מובילים, למעט המספר 0 עצמו.
  • :: תו נקודתיים מילולי המשמש כמפריד.
  • [string]: מחרוזת הבתים הגולמית של המטען. היא יכולה לכלול כל מידע בינארי, ובכלל זה בתי אפס, נקודתיים, פסיקים וירידות שורה.
  • ,: תו פסיק מילולי המסמן סיום.

מחרוזת ה־ASCII ‏hello world! תופסת 12 בתים ומקודדת כך. סופרים בתים מקודדים, לא תווי Unicode: מחרוזת עברית ב־UTF-8 משתמשת בדרך כלל ביותר מבית אחד לתו. [מקור 1] [מקור 2]

12:hello world!,

ומה אם רוצים לשלוח מחרוזת ריקה? פשוט:

0:,

מכיוון שאורך המטען מוצהר מראש, המקבל יודע בדיוק כמה בתים לקרוא אחרי הנקודתיים. אין צורך לסרוק את המטען בחיפוש מפרידים, לטפל בתווי מילוט או לדאוג לסדר בתים בינארי.

תכונות שימושיות של Netstrings [מקור 1]

Netstrings יכולים למסגר בתים של JSON או Protobuf; הם אינם מחליפים את מודל הנתונים, כלי הסכמה והמערכת שסביב הפורמטים האלה. היתרונות שלהם נוגעים למסגור ולאימות פשוטים. [מקור 1]

1 מגבלות משאבים ולא חסינות מפני DoS [מקור 1]

מפענח מפרידים ללא מגבלות יכול למצות זיכרון בזמן שהוא ממתין למסיים. זה שונה מגלישת חוצץ המשחיתה זיכרון. גם מפענח מפרידים וגם מפענח קידומת אורך יכולים להיות לא בטוחים אם הם מקבלים קלט בלתי מוגבל או מטפלים באופן שגוי בחישובים. [מקור 1]

מקבל יכול לדחות מטען מוצהר של 50 MB כשהמדיניות מתירה לכל היותר 1 MB, לפני הקצאת המטען. עליו גם להגביל את מספר ספרות הקידומת, לאמת כל בית, לדחות אפסים מובילים לא תקניים, לקבוע מועדי סיום לחיבור ולמסגרת ולהגביל לקוחות מקבילים. כותרת קצרה ותקינה אינה עוצרת שולח איטי או הצפת חיבורים. [מקור 1] [מקור 2]

2 תצוגות חוצץ ופשרות ההעתקה [מקור 1]

אם מסגרת שלמה כבר נמצאת ברצף בזיכרון, Buffer.subarray יכול לחשוף את המטען ללא העתקה. הרכבת קלט מפוצל עדיין עשויה להעתיק נתונים; Buffer.concat מעתיק בתים לחוצץ חדש, ותצוגה קטנה יכולה להחזיק הקצאה גדולה בזיכרון. הדוגמה המתוקנת בהמשך מקצה בכוונה לכל היותר מטען מוגבל אחד ומעתיקה אליו את הבתים הנכנסים. היא אינה מימוש ללא העתקה. [מקור 1]

3 עצמאות משכבת התעבורה ומהארכיטקטורה

מכיוון שקידומת האורך נשלחת כטקסט ASCII, למשל 1024, אין צורך לדאוג אם השולח הוא מחשב מרכזי בן 32 סיביות בסדר big-endian או שבב ARM בן 64 סיביות בסדר little-endian. הקידוד אחיד, קריא וקל מאוד לאיתור תקלות באמצעות כלי רשת מקובלים כמו netcat, ‏tcpdump או wireshark.

מימוש מפענח Netstring ב־Node.js וב־TypeScript

דוגמת ה־TypeScript הבאה לא הורצה. היא משתמשת במכונת מצבים של אורך, מטען ופסיק, מאמתת ספרות ASCII ללא parseInt, דוחה אפסים מובילים, מגבילה כל מטען ל־1 MiB ומזהה קלט קטוע. כל חיבור מחזיק לכל היותר מטען מוגבל אחד, לא כולל חוצצי השקע של Node ונתונים שהיישום שומר. יש לבדוק ולסקור אותה לפני שימוש; אין זו הבטחת אבטחה לסביבת ייצור. [מקור 1] [מקור 2] [מקור 3]

// Runtime-untested illustrative TypeScript; review before deployment.
import { Buffer } from 'node:buffer';

export class NetstringParser {
  private length = 0;
  private digits = 0;
  private leadingZero = false;
  private state: 'length' | 'payload' | 'comma' = 'length';
  private payload = Buffer.alloc(0);
  private offset = 0;
  public frameStartedAt: number | null = null;
  constructor(private onMessage: (payload: Buffer) => void,
              private maxPayload = 1024 * 1024) {
    if (!Number.isSafeInteger(maxPayload) || maxPayload < 0) {
      throw new Error('Invalid payload limit');
    }
  }
  write(chunk: Buffer): void {
    let i = 0;
    while (i < chunk.length) {
      if (this.state === 'length') {
        const byte = chunk[i++];
        if (this.digits === 0) this.frameStartedAt = performance.now();
        if (byte === 0x3a) {
          if (this.digits === 0) throw new Error('Empty length');
          this.payload = Buffer.alloc(this.length);
          this.offset = 0;
          this.state = this.length === 0 ? 'comma' : 'payload';
        } else {
          if (byte < 0x30 || byte > 0x39 || this.leadingZero) {
            throw new Error('Invalid length');
          }
          if (++this.digits > String(this.maxPayload).length) {
            throw new Error('Length prefix too long');
          }
          this.leadingZero = this.digits === 1 && byte === 0x30;
          this.length = this.length * 10 + byte - 0x30;
          if (!Number.isSafeInteger(this.length) || this.length > this.maxPayload) {
            throw new Error('Payload too large');
          }
        }
      } else if (this.state === 'payload') {
        const count = Math.min(chunk.length - i, this.length - this.offset);
        chunk.copy(this.payload, this.offset, i, i + count);
        this.offset += count;
        i += count;
        if (this.offset === this.length) this.state = 'comma';
      } else {
        if (chunk[i++] !== 0x2c) throw new Error('Missing comma');
        const complete = this.payload;
        this.state = 'length';
        this.length = this.digits = this.offset = 0;
        this.leadingZero = false;
        this.payload = Buffer.alloc(0);
        this.frameStartedAt = null;
        this.onMessage(complete); // Callback must not reenter write().
      }
    }
  }
  end(): void {
    if (this.state !== 'length' || this.digits !== 0) {
      throw new Error('Truncated netstring');
    }
  }
}

דוגמת שרת האישור המקומי הבאה לא הורצה. היא מוסיפה מגבלות זמן לחוסר פעילות ולמסגרת, סוגרת קלט פגום ומשהה קריאות שקע עתידיות כשהפלט דורש ויסות עומס; פענוח המקטע הנוכחי ממשיך. היא מאזינה ב־loopback בלבד. אימות, הצפנה, מגבלות חיבורים וקצב כלליות, סגירה מסודרת ובדיקות מול קלט עוין נשארים חלק נפרד מעבודת הפריסה. [מקור 1]

// Runtime-untested; NetstringParser is defined above.
import * as net from 'node:net';

const server = net.createServer((socket) => {
  const parser = new NetstringParser((_payload) => {
    if (!socket.write('2:OK,')) socket.pause();
  });
  socket.on('drain', () => socket.resume());
  socket.setTimeout(5000, () => socket.destroy());
  const timer = setInterval(() => {
    const started = parser.frameStartedAt;
    if (started !== null && performance.now() - started > 5000) socket.destroy();
  }, 250);
  timer.unref();
  socket.on('data', (chunk) => {
    try { parser.write(chunk); } catch { socket.destroy(); }
  });
  socket.on('end', () => {
    try { parser.end(); } catch { socket.destroy(); }
  });
  socket.on('error', () => socket.destroy());
  socket.on('close', () => clearInterval(timer));
});
server.on('error', (error) => console.error(error.message));
server.listen(8080, '127.0.0.1');

היכן משתמשים כיום ב־Netstrings

יש להבחין בין שימושים מתועדים ב־Netstrings, קידודים אחרים עם קידומת אורך ויישומים אפשריים: [מקור 1]

  • SCGI: פרוטוקול בין שרת אינטרנט ליישום, שבו כותרות הבקשה מועברות ב־netstring ואחריהן גוף בקשה נפרד. אין זה מדויק לתאר את כל חלקי SCGI כ־netstring. [מקור 1]
  • המפרט של Bernstein הוא מקור ישיר לפורמט. הטענה המקורית שלפיה Qmail ו־djbdns משתמשים בלעדית ב־Netstrings לתקשורת בין תהליכים אינה מבוססת כאן, ואין להשתמש בה כטענה על אימוץ או אבטחה. [מקור 1]
  • RESP: גם מחרוזות bulk של Redis משתמשות באורך מפורש בבתים, אך ל־RESP סימוני טיפוסים, כללי CRLF וטיפוסי אוסף משלו. זהו פרוטוקול נפרד, לא גרסה תואמת של Netstrings. [מקור 1]
  • תכנון IoT ומערכות משובצות: Netstrings הם אפשרות כאשר מפענח קטן והודעות מוגבלות מתאימים לדרישות. ההתאמה תלויה בגודל המטען, בזיכרון RAM, בתעבורה ובמגבלות ההתקן; אין כאן טענה לאימוץ רחב או לפענוח במקום. [מקור 1]

לסיכום מתי להשתמש ב־Netstrings

Netstrings אינם תחליף אוניברסלי לפרוטוקולים מקובלים. ממשקי API לדפדפן משתמשים לעיתים קרובות ב־JSON מעל HTTP, ואילו RPC או פורמטי נתונים מבוססי סכמה עשויים להתאים לשירותים מורכבים. בחרו בנפרד את התעבורה, המסגור, הסריאליזציה, האימות ויכולת התצפית הנחוצים לכם. [מקור 1]

עם זאת, אם אתם בונים:

  • מיקרו־שירותים פנימיים קלי משקל
  • תהליכי daemon עתירי ביצועים המבוססים על TCP
  • תקשורת בין תהליכים, IPC, דרך Unix domain sockets
  • צינורות נתונים למערכות IoT או משובצות עם תקציב מחשוב מוגבל

במקרים האלה כדאי לבחון Netstrings כשנחוץ פורמט פשוט למסגרות בתים. הקידוד פשוט, אך פענוח בטוח בייצור דורש מגבלות, מועדי סיום, בקרת משאבים ובדיקות. מספר שורות הקוד אינו מדד לאבטחה. [מקור 1]

מה דעתכם? האם השתמשתם ב־Netstrings או בפרוטוקול דומה עם קידומת אורך במערכות שלכם? או שאתם מעדיפים להישאר עם זרמי JSON או msgpack? כתבו בתגובות!

עד הפעם הבאה, תכנות מהנה!

מקורות ראשוניים לתיקונים

Post a Comment

Previous Post Next Post