לדלג לתוכן

צינורות ופרוטוקול מקומי

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

נשתמש ב־named pipe עם פרוטוקול קבוע. בפרויקט הפרק נבנה שרת שמחבר אותו ליומן. כאן נכתוב לקוח מלא שיכול לדבר עם השרת, ונבחן כיצד קריאה חופפת נשארת בחיים גם בזמן timeout.

מסגרת בקשה קבועה

הבקשה מכילה עשרים וארבעה בתים: magic בן ארבעה בתים, גרסה וסוג פעולה בני שני בתים, מזהה בקשה בן שמונה בתים, מפתח וערך בני ארבעה בתים כל אחד. כל המספרים נכתבים ב־little endian. התשובה באותו גודל מכילה magic אחר, סטטוס, מזהה בקשה, ערך ושדה דגלים.

התוכנית הבאה שולחת בקשת put למפתח שבע עם ערך תשעים ותשע ומזהה בקשה אחד. היא דורשת ששרת הפרויקט כבר ירוץ באותה מכונה:

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iostream>

struct Handle {
    HANDLE value;
    explicit Handle(HANDLE v) : value(v) {}
    ~Handle() {
        if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value);
    }
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;
};

bool transfer(HANDLE pipe, bool writing, void* data, DWORD count,
              DWORD& transferred, DWORD& error) {
    Handle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
    if (!event.value) { error = GetLastError(); return false; }
    OVERLAPPED operation{};
    operation.hEvent = event.value;
    DWORD ignored = 0;
    const BOOL ok = writing
        ? WriteFile(pipe, data, count, &ignored, &operation)
        : ReadFile(pipe, data, count, &ignored, &operation);
    if (ok) {
        transferred = ignored;
        error = ERROR_SUCCESS;
        return true;
    }
    if (!ok) {
        error = GetLastError();
        if (error != ERROR_IO_PENDING) return false;
    }
    const DWORD wait = WaitForSingleObject(event.value, 2000);
    if (wait != WAIT_OBJECT_0) {
        if (!CancelIoEx(pipe, &operation) &&
            GetLastError() != ERROR_NOT_FOUND)
            std::cerr << "cancel request failed\n";
    }
    const BOOL completed = GetOverlappedResult(
        pipe, &operation, &transferred, TRUE);
    error = completed ? ERROR_SUCCESS : GetLastError();
    return completed != FALSE;
}

void put(std::array<std::byte, 24>& bytes, std::size_t offset,
         std::uint64_t value, unsigned width) {
    for (unsigned i = 0; i < width; ++i)
        bytes[offset + i] = std::byte((value >> (i * 8)) & 0xffu);
}

int main() {
    constexpr auto name = L"\\\\.\\pipe\\AmitTech.WalLab";
    if (!WaitNamedPipeW(name, 2000)) {
        std::cerr << "server unavailable: " << GetLastError() << '\n';
        return 1;
    }
    Handle pipe(CreateFileW(name, GENERIC_READ | GENERIC_WRITE, 0,
        nullptr, OPEN_EXISTING,
        FILE_FLAG_OVERLAPPED | SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION,
        nullptr));
    if (pipe.value == INVALID_HANDLE_VALUE) return 1;
    DWORD mode = PIPE_READMODE_MESSAGE;
    if (!SetNamedPipeHandleState(pipe.value, &mode, nullptr, nullptr)) return 1;

    std::array<std::byte, 24> request{};
    request[0] = std::byte{'K'}; request[1] = std::byte{'V'};
    request[2] = std::byte{'P'}; request[3] = std::byte{'1'};
    put(request, 4, 1, 2);
    put(request, 6, 1, 2);
    put(request, 8, 1, 8);
    put(request, 16, 7, 4);
    put(request, 20, 99, 4);
    DWORD count = 0, error = 0;
    if (!transfer(pipe.value, true, request.data(), 24, count, error) ||
        count != 24) {
        std::cerr << "write: " << error << '\n';
        return 1;
    }
    std::array<std::byte, 24> response{};
    if (!transfer(pipe.value, false, response.data(), 24, count, error) ||
        count != 24) {
        std::cerr << "read: " << error << '\n';
        return 1;
    }
    if (response[0] != std::byte{'K'} || response[1] != std::byte{'V'} ||
        response[2] != std::byte{'R'} || response[3] != std::byte{'1'})
        return 1;
    std::byte acknowledgment{0xac};
    if (!transfer(pipe.value, true, &acknowledgment, 1, count, error) ||
        count != 1) std::cerr << "acknowledgment failed\n";
    std::cout << "received 24-byte response; validate fields next\n";
}

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

הודעות לעומת זרם בתים

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

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

חיבור ואבטחה

השרת ייצור את ה־pipe עם DACL שמאפשרת רק למשתמש המעבדה ול־SYSTEM, עם PIPE_REJECT_REMOTE_CLIENTS ועם FILE_FLAG_FIRST_PIPE_INSTANCE. ההגבלות מתאימות לשירות המעבדה המקומי, אך אינן מזהות בהכרח יישום מסוים בתוך אותו חשבון משתמש.

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

הלקוח משתמש ב־SECURITY_IDENTIFICATION כדי להגביל את רמת ה־impersonation המותרת לפי חוזה SQOS. אין להעניק לשרת יכולת פעולה בשם הלקוח בלי צורך מפורש. הדבר אינו מחליף אימות תוכן או הרשאות על פעולות עסקיות.

לקוח איטי וביטול

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

שרת צריך להגביל מספר חיבורים, גודל מסגרת וזמן המתנה. מעבר ל־overlapped I/O מאפשר לטפל בכמה לקוחות בלי thread לכל המתנה, אך אינו מבטל את הצורך בגבולות. בפרויקט נתחיל בלקוח אחד בכל רגע כדי להתרכז בעמידות ובפרוטוקול.

קריאה להעמקה

התיעוד של אבטחת named pipes ושל מצבי פתיחה מגדיר את ההרשאות. החוזים של CreateNamedPipe מגדירים את המצבים שבהם השרת משתמש.