diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c9b95cc --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,70 @@ +name: Cross-Platform Build + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + architecture: [x64] + include: + - os: ubuntu-latest + architecture: x64 + makefile: build/unix/x86_64_linux.mk + target: libsnap7.so + - os: windows-latest + architecture: x64 + makefile: build/windows/MinGW64/Makefile + target: snap7.dll + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up build environment (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential + + - name: Set up build environment (Windows) + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: mingw-w64-x86_64-gcc mingw-w64-x86_64-binutils make + + - name: Build (Linux) + if: runner.os == 'Linux' + working-directory: ${{ github.workspace }} + run: | + make -f ${{ matrix.makefile }} + ls -la build/bin/x86_64-linux/ + + - name: Build (Windows) + if: runner.os == 'Windows' + working-directory: ${{ github.workspace }}/build/windows/MinGW64 + shell: msys2 {0} + run: | + make + ls -la ../../bin/Legacy/win64/ + + - name: Upload Linux artifact + if: runner.os == 'Linux' + uses: actions/upload-artifact@v4 + with: + name: snap7-linux-x64 + path: build/bin/x86_64-linux/libsnap7.so + + - name: Upload Windows artifact + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: snap7-windows-x64 + path: build/bin/Legacy/win64/snap7.dll \ No newline at end of file diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml new file mode 100644 index 0000000..0518730 --- /dev/null +++ b/.github/workflows/makefile.yml @@ -0,0 +1,37 @@ +name: Build projects with Make + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + architecture: [x64] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up build environment (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential + + - name: Set up build environment (Windows) + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: mingw-w64-x86_64-gcc mingw-w64-x86_64-binutils make + + - name: Build + run: | + cd build + make \ No newline at end of file diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 0000000..2bb7664 --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,69 @@ +name: Smoke Test + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + smoke-test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + architecture: [x64] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up build environment (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential + + - name: Set up build environment (Windows) + if: runner.os == 'Windows' + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: mingw-w64-x86_64-gcc mingw-w64-x86_64-binutils make + + - name: Build library (Linux) + if: runner.os == 'Linux' + working-directory: ${{ github.workspace }} + run: make -f build/unix/x86_64_linux.mk + + - name: Build library (Windows) + if: runner.os == 'Windows' + working-directory: ${{ github.workspace }}/build/windows/MinGW64 + shell: msys2 {0} + run: make + + - name: Build loopback test (Linux) + if: runner.os == 'Linux' + working-directory: ${{ github.workspace }}/examples/cpp/x86_64-linux + run: make loopback_test + + - name: Build loopback test (Windows) + if: runner.os == 'Windows' + working-directory: ${{ github.workspace }}/examples/cpp/x86_64-linux + shell: msys2 {0} + run: make loopback_test + + - name: Run Linux loopback test + if: runner.os == 'Linux' + working-directory: ${{ github.workspace }}/examples/cpp/x86_64-linux + run: | + # Start test in background and give it time to run + timeout 60 ./loopback_test + + - name: Run Windows loopback test + if: runner.os == 'Windows' + working-directory: ${{ github.workspace }}/examples/cpp/x86_64-linux + shell: msys2 {0} + run: | + # Start test and give it time to run + timeout 60 ./loopback_test.exe \ No newline at end of file diff --git a/README.md b/README.md index 739b935..c58a9ef 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,35 @@ For more details I refer to the commit history. Please note that this fork is currently at version 1.4.0. Cleanup, more fixes and documentation will follow. + +## License + +This repository includes the applicable license texts in the repository root: + +- [GPL license text](gpl.txt) +- [LGPL v3.0 license text](lgpl-3.0.txt) + +Please review the applicable license terms before using, modifying, +or redistributing this software. + +## Fork Status + +This repository is a community-maintained fork of +[SCADACS/snap7](https://github.com/SCADACS/snap7). + +The original Snap7 project and its contributors retain credit for the +upstream codebase. This fork adds independently maintained documentation, +build, testing, security, and integration improvements. + +See the repository history and license texts for applicable notices. + +## Security + +For security considerations when deploying Snap7 in industrial environments, +see [SECURITY.md](SECURITY.md), [docs/secure-deployment.md](docs/secure-deployment.md), +and [docs/industrial-networking.md](docs/industrial-networking.md). + +## Continuous Integration + +This project includes GitHub Actions workflows for cross-platform builds +and automated smoke tests on Linux and Windows. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..308464e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Reporting a Vulnerability + +Please avoid publishing suspected security vulnerabilities in public issue trackers before maintainers have had an opportunity to assess them. + +When a private reporting channel is available, use that channel and include: + +- A clear description of the issue +- Affected versions or commits +- Steps to reproduce the issue +- Potential impact +- Any proposed mitigation, if available \ No newline at end of file diff --git a/docs/industrial-networking.md b/docs/industrial-networking.md new file mode 100644 index 0000000..0e5dc70 --- /dev/null +++ b/docs/industrial-networking.md @@ -0,0 +1,49 @@ +# Industrial Networking Guidance + +This document provides guidance on network architecture and communication patterns when using Snap7 in industrial environments. + +## Network Segmentation + +Industrial control systems should be separated from enterprise IT networks using: + +- **DMZ / Industrial DMZ**: A controlled boundary between IT and OT networks +- **Firewalls**: Stateful inspection firewalls with rules allowing only necessary protocols and ports +- **VLANs**: Logical separation of traffic types (e.g., HMI, historian, engineering, safety) +- **Unidirectional Gateways (Data Diodes)**: For high-security environments requiring data flow from OT to IT only + +## Communication Protocols + +Snap7 uses the Siemens S7 protocol over ISO-on-TCP (RFC 1006) typically on port 102. Consider: + +- Restricting port 102 access to authorized engineering stations only +- Using non-standard ports where possible to reduce automated scanning +- Implementing port knocking or SPA (Single Packet Authorization) for additional obscurity + +## Redundancy and Availability + +- Design network paths with redundancy for critical communication +- Implement monitoring for communication loss and latency anomalies +- Consider PLC-side communication load when multiple clients connect +- Use Snap7's connection management features to handle reconnections gracefully + +## Monitoring and Logging + +- Log all connection attempts (successful and failed) +- Monitor for unusual traffic patterns or unexpected PLC commands +- Correlate network logs with PLC diagnostic buffers +- Set up alerts for communication failures on critical assets + +## Remote Access + +If remote access is required: + +- Use a dedicated VPN with multi-factor authentication +- Implement jump hosts / bastion hosts for engineering access +- Record and audit all remote sessions +- Enforce time-limited access with automatic revocation + +## References + +- IEC 62443 Series - Industrial communication network security +- NIST SP 800-82 - Guide to Industrial Control Systems Security +- ISA/IEC 62443-3-3 - System security requirements and security levels \ No newline at end of file diff --git a/docs/secure-deployment.md b/docs/secure-deployment.md new file mode 100644 index 0000000..9c1eba9 --- /dev/null +++ b/docs/secure-deployment.md @@ -0,0 +1,29 @@ +# Secure Deployment Guide + +This guide provides security-focused deployment recommendations for systems using Snap7 to communicate with Siemens S7 PLCs. + +## Network Architecture + +- PLCs should not be exposed directly to the internet. +- Use network segmentation and firewall rules to isolate OT networks from IT networks. +- Use VPN or controlled remote-access mechanisms for remote engineering access. +- Limit access to authorized engineering and service networks only. +- Use least-privilege access where supported by the PLC and network infrastructure. + +## Operational Security + +- Treat PLC write operations as high-risk actions. +- Separate development/test devices from production assets. +- Log, monitor, and review communication failures and unexpected commands. +- Implement change management procedures for any modifications to PLC programs or configuration. + +## Important Limitations + +Snap7 is not a substitute for OT network architecture and operational security controls. It provides communication capabilities but does not implement: + +- Network-level authentication or encryption (relies on underlying transport) +- Industrial firewall or intrusion detection +- Safety system functions +- Compliance with specific industrial security standards (e.g., IEC 62443) + +These controls must be implemented at the network and system architecture level. \ No newline at end of file diff --git a/examples/cpp/loopback_test.cpp b/examples/cpp/loopback_test.cpp new file mode 100644 index 0000000..29695d7 --- /dev/null +++ b/examples/cpp/loopback_test.cpp @@ -0,0 +1,346 @@ +/*=============================================================================| +| PROJECT SNAP7 1.4.0 | +|==============================================================================| +| Copyright (C) 2013, 2014 Davide Nardella | +| All rights reserved. | +|==============================================================================| +| SNAP7 is free software: you can redistribute it and/or modify | +| it under the terms of the Lesser GNU General Public License as published by | +| the Free Software Foundation, either version 3 of the License, or | +| (at your option) any later version. | +| | +| It means that you can distribute your commercial software linked with | +| SNAP7 without the requirement to distribute the source code of your | +| application and without the requirement that your application be itself | +| distributed under LGPL. | +| | +| SNAP7 is distributed in the hope that it will be useful, | +| but WITHOUT ANY WARRANTY; without even the implied warranty of | +| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | +| Lesser GNU General Public License for more details. | +| | +| You should have received a copy of the GNU General Public License and a | +| copy of Lesser GNU General Public License along with Snap7. | +| If not, see http://www.gnu.org/licenses/ | +|==============================================================================| +| | +| Loopback Smoke Test | +| Tests local client/server communication without requiring real PLC hardware.| +|=============================================================================*/ +#include +#include +#include +#include +#include +#include "snap7.h" + +#ifdef OS_WINDOWS +# define WIN32_LEAN_AND_MEAN +# include +#else +# include +#endif + +// Test configuration - uses localhost and non-privileged port +static const char* TEST_HOST = "127.0.0.1"; +static const int TEST_PORT = 1102; // Non-privileged port for testing +static const int TEST_RACK = 0; +static const int TEST_SLOT = 1; + +// Test data +static byte test_db_data[256]; +static bool server_ready = false; +static int server_error = 0; + +// Server event callback +void S7API ServerEventCallback(void* usrPtr, PSrvEvent PEvent, int Size) { + // Print events for debugging + printf("[SERVER] %s\n", SrvEventText(PEvent).c_str()); +} + +// Server read event callback +void S7API ServerReadEventCallback(void* usrPtr, PSrvEvent PEvent, int Size) { + if (PEvent->EvtParam1 == S7AreaDB) { + int db_num = PEvent->EvtParam2; + if (db_num == 1) { + memset(test_db_data, 0xAA, sizeof(test_db_data)); + } + } +} + +// Server thread function +void ServerThread() { + TS7Server* Server = new TS7Server(); + + // Register a test DB + Server->RegisterArea(srvAreaDB, 1, test_db_data, sizeof(test_db_data)); + + // Set callbacks + Server->SetEventsCallback(ServerEventCallback, NULL); + Server->SetReadEventsCallback(ServerReadEventCallback, NULL); + + // Start server on localhost with test port + server_error = Server->StartTo(TEST_HOST); + if (server_error == 0) { + server_ready = true; + printf("[SERVER] Started on %s:%d\n", TEST_HOST, TEST_PORT); + + // Keep server running + while (server_ready) { +#ifdef OS_WINDOWS + Sleep(100); +#else + usleep(100000); +#endif + } + } else { + printf("[SERVER] Failed to start: %s\n", SrvErrorText(server_error).c_str()); + } + + Server->Stop(); + delete Server; +} + +bool Check(int Result, const char* Function) { + printf("\n+-----------------------------------------------------\n"); + printf("| %s\n", Function); + printf("+-----------------------------------------------------\n"); + if (Result == 0) { + printf("| Result : OK\n"); + printf("+-----------------------------------------------------\n"); + return true; + } else { + printf("| ERROR !!!\n"); + if (Result < 0) + printf("| Library Error (-1)\n"); + else + printf("| %s\n", CliErrorText(Result).c_str()); + printf("+-----------------------------------------------------\n"); + return false; + } +} + +// SysSleep for cross-platform sleep +void SysSleep(longword Delay_ms) { +#ifdef OS_WINDOWS + Sleep(Delay_ms); +#else + struct timespec ts; + ts.tv_sec = (time_t)(Delay_ms / 1000); + ts.tv_nsec = (long)((Delay_ms - ts.tv_sec * 1000) * 1000000); + nanosleep(&ts, NULL); +#endif +} + +int main() { + int ok = 0; + int ko = 0; + + printf("=== Snap7 Loopback Smoke Test ===\n"); + printf("Host: %s, Port: %d, Rack: %d, Slot: %d\n\n", TEST_HOST, TEST_PORT, TEST_RACK, TEST_SLOT); + + // Start server in background thread + std::thread server_thread(ServerThread); + + // Wait for server to be ready + for (int i = 0; i < 50 && !server_ready; i++) { + SysSleep(100); + } + + if (!server_ready) { + printf("ERROR: Server failed to start within timeout\n"); + server_ready = false; // Signal server thread to exit + server_thread.join(); + return 1; + } + + // Create client + TS7Client* Client = new TS7Client(); + + // Connect to local server + int res = Client->ConnectTo(TEST_HOST, TEST_RACK, TEST_SLOT); + if (Check(res, "Client Connect")) { + printf(" Connected to : %s (Rack=%d, Slot=%d)\n", TEST_HOST, TEST_RACK, TEST_SLOT); + printf(" PDU Requested : %d bytes\n", Client->PDURequested()); + printf(" PDU Negotiated : %d bytes\n", Client->PDULength()); + ok++; + } else { + ko++; + } + + if (res == 0) { + // Test 1: Read DB1 (should return 0xAA pattern from server callback) + byte buffer[256]; + int size = sizeof(buffer); + res = Client->DBRead(1, 0, size, buffer); + if (Check(res, "DBRead (DB1)")) { + bool pattern_ok = true; + for (int i = 0; i < size; i++) { + if (buffer[i] != 0xAA) { + pattern_ok = false; + break; + } + } + if (pattern_ok) { + printf(" Data pattern verified: 0xAA\n"); + ok++; + } else { + printf(" ERROR: Data pattern mismatch\n"); + ko++; + } + } else { + ko++; + } + + // Test 2: Write and read back + byte write_data[32]; + for (int i = 0; i < 32; i++) write_data[i] = (byte)(i + 1); + + res = Client->DBWrite(1, 0, 32, write_data); + if (Check(res, "DBWrite (DB1)")) { + ok++; + } else { + ko++; + } + + memset(buffer, 0, sizeof(buffer)); + res = Client->DBRead(1, 0, 32, buffer); + if (Check(res, "DBRead after Write (DB1)")) { + bool data_ok = true; + for (int i = 0; i < 32; i++) { + if (buffer[i] != write_data[i]) { + data_ok = false; + break; + } + } + if (data_ok) { + printf(" Write/Read verified\n"); + ok++; + } else { + printf(" ERROR: Write/Read data mismatch\n"); + ko++; + } + } else { + ko++; + } + + // Test 3: Get CPU info + TS7CpuInfo cpu_info; + res = Client->GetCpuInfo(&cpu_info); + if (Check(res, "GetCpuInfo")) { + printf(" Module Type : %s\n", cpu_info.ModuleTypeName); + printf(" Serial Number : %s\n", cpu_info.SerialNumber); + printf(" AS Name : %s\n", cpu_info.ASName); + printf(" Module Name : %s\n", cpu_info.ModuleName); + ok++; + } else { + ko++; + } + + // Test 4: Get Order Code + TS7OrderCode order_code; + res = Client->GetOrderCode(&order_code); + if (Check(res, "GetOrderCode")) { + printf(" Order Code : %s\n", order_code.Code); + printf(" Version : %d.%d.%d\n", order_code.V1, order_code.V2, order_code.V3); + ok++; + } else { + ko++; + } + + // Test 5: Get CP Info + TS7CpInfo cp_info; + res = Client->GetCpInfo(&cp_info); + if (Check(res, "GetCpInfo")) { + printf(" Max PDU Length : %d bytes\n", cp_info.MaxPduLengt); + printf(" Max Connections : %d\n", cp_info.MaxConnections); + printf(" Max MPI Rate : %d bps\n", cp_info.MaxMpiRate); + printf(" Max Bus Rate : %d bps\n", cp_info.MaxBusRate); + ok++; + } else { + ko++; + } + + // Test 6: PLC Status + int status = Client->PlcStatus(); + if (Check(res, "PlcStatus")) { + printf(" CPU Status : "); + switch (status) { + case S7CpuStatusRun: printf("RUN\n"); break; + case S7CpuStatusStop: printf("STOP\n"); break; + default: printf("UNKNOWN\n"); break; + } + ok++; + } else { + ko++; + } + + // Test 7: List Blocks + TS7BlocksList blocks; + res = Client->ListBlocks(&blocks); + if (Check(res, "ListBlocks")) { + printf(" OB Count : %d\n", blocks.OBCount); + printf(" FB Count : %d\n", blocks.FBCount); + printf(" FC Count : %d\n", blocks.FCCount); + printf(" SFB Count : %d\n", blocks.SFBCount); + printf(" SFC Count : %d\n", blocks.SFCCount); + printf(" DB Count : %d\n", blocks.DBCount); + printf(" SDB Count : %d\n", blocks.SDBCount); + ok++; + } else { + ko++; + } + + // Test 8: Read SZL (System Zone List) + byte szl_buffer[1024]; + int szl_size = sizeof(szl_buffer); + PS7SZL szl = (PS7SZL)szl_buffer; + res = Client->ReadSZL(0x0011, 0x0000, szl, &szl_size); + if (Check(res, "ReadSZL (ID:0x0011 IDX:0x0000)")) { + printf(" LENTHDR : %d\n", szl->Header.LENTHDR); + printf(" N_DR : %d\n", szl->Header.N_DR); + ok++; + } else { + ko++; + } + + // Test 9: Connection refusal (connect to non-listening port) + TS7Client* Client2 = new TS7Client(); + res = Client2->ConnectTo(TEST_HOST, TEST_RACK, TEST_SLOT + 10); // Different slot, should fail + if (res != 0) { + printf("\n+-----------------------------------------------------\n"); + printf("| Connection Refusal Test\n"); + printf("+-----------------------------------------------------\n"); + printf("| Result : OK (correctly refused)\n"); + printf("+-----------------------------------------------------\n"); + ok++; + } else { + printf("\n+-----------------------------------------------------\n"); + printf("| Connection Refusal Test\n"); + printf("+-----------------------------------------------------\n"); + printf("| ERROR: Expected connection refusal\n"); + printf("+-----------------------------------------------------\n"); + ko++; + } + delete Client2; + + // Disconnect + Client->Disconnect(); + printf("\n[CLIENT] Disconnected\n"); + } + + // Cleanup + delete Client; + + // Stop server + server_ready = false; + server_thread.join(); + + // Summary + printf("\n=== Test Summary ===\n"); + printf("Performed : %d\n", (ok + ko)); + printf("Passed : %d\n", ok); + printf("Failed : %d\n", ko); + + return (ko == 0) ? 0 : 1; +} \ No newline at end of file diff --git a/examples/cpp/x86_64-linux/makefile b/examples/cpp/x86_64-linux/makefile index 9573e49..6f6b25f 100644 --- a/examples/cpp/x86_64-linux/makefile +++ b/examples/cpp/x86_64-linux/makefile @@ -21,7 +21,8 @@ all: $(CXX) $(CXXFLAGS) -o server ../server.cpp ../$(Wrapper) $(Libs) $(CXX) $(CXXFLAGS) -o srv_resourceless ../srv_resourceless.cpp ../$(Wrapper) $(Libs) $(CXX) $(CXXFLAGS) -o apartner ../apartner.cpp ../$(Wrapper) $(Libs) - $(CXX) $(CXXFLAGS) -o ppartner ../ppartner.cpp ../$(Wrapper) $(Libs) + $(CXX) $(CXXFLAGS) -o ppartner ../ppartner.cpp ../$(Wrapper) $(Libs) + $(CXX) $(CXXFLAGS) -std=c++11 -pthread -o loopback_test ../loopback_test.cpp ../$(Wrapper) $(Libs) clean: $(RM) client @@ -29,3 +30,4 @@ clean: $(RM) srv_resourceless $(RM) apartner $(RM) ppartner + $(RM) loopback_test