This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 1356ec14b9 [Feature][Connector-V2] Add SNMPv2c SET sink connector 
(#12011)
1356ec14b9 is described below

commit 1356ec14b9bd7ffd5fbfe2f4d20013816148f3ca
Author: Goutam Adwant <[email protected]>
AuthorDate: Tue Sep 8 16:39:52 2026 +0000

    [Feature][Connector-V2] Add SNMPv2c SET sink connector (#12011)
    
    Signed-off-by: goutamadwant <[email protected]>
---
 .github/workflows/labeler/label-scope-conf.yml     |   7 +
 docs/en/connectors/changelog/connector-snmp.md     |   1 +
 docs/en/connectors/sink/SNMP.md                    | 137 +++++++++
 docs/zh/connectors/changelog/connector-snmp.md     |   1 +
 docs/zh/connectors/sink/SNMP.md                    | 135 +++++++++
 plugin-mapping.properties                          |   1 +
 .../seatunnel/snmp/client/SnmpTargetFactory.java   |  51 ++++
 .../{SnmpSourceOptions.java => SnmpOptions.java}   |  19 +-
 .../seatunnel/snmp/config/SnmpSinkConfig.java      | 131 +++++++++
 .../seatunnel/snmp/config/SnmpSinkOptions.java     |  53 ++++
 .../seatunnel/snmp/config/SnmpSourceConfig.java    |  39 +--
 .../seatunnel/snmp/config/SnmpSourceOptions.java   |  29 +-
 .../SnmpTargetConfig.java}                         |  29 +-
 .../snmp/exception/SnmpConnectorErrorCode.java     |   6 +-
 .../Snmp4jSetClient.java}                          |  67 ++---
 .../SnmpSetClient.java}                            |  27 +-
 .../SnmpSetRequest.java}                           |  29 +-
 .../connectors/seatunnel/snmp/sink/SnmpSink.java   |  59 ++++
 .../seatunnel/snmp/sink/SnmpSinkFactory.java       |  70 +++++
 .../seatunnel/snmp/sink/SnmpSinkRowConverter.java  | 316 +++++++++++++++++++++
 .../seatunnel/snmp/sink/SnmpSinkWriter.java        |  93 ++++++
 .../seatunnel/snmp/source/Snmp4jClient.java        |  13 +-
 .../snmp/client/SnmpTargetFactoryTest.java         |  47 +++
 .../seatunnel/snmp/config/SnmpSinkConfigTest.java  | 119 ++++++++
 .../snmp/config/SnmpSourceConfigTest.java          |  34 +++
 .../seatunnel/snmp/sink/Snmp4jSetClientTest.java   | 160 +++++++++++
 .../seatunnel/snmp/sink/SnmpSinkFactoryTest.java   | 189 ++++++++++++
 .../snmp/sink/SnmpSinkRowConverterTest.java        | 246 ++++++++++++++++
 .../seatunnel/snmp/sink/SnmpSinkWriterTest.java    | 133 +++++++++
 .../core/starter/utils/ConfigShadeUtils.java       |   2 +-
 .../core/starter/utils/ConfigBuilderTest.java      |  19 ++
 .../connector-snmp-e2e/pom.xml                     |  42 +++
 .../seatunnel/e2e/connector/snmp/SnmpAgent.java    |  95 +++++++
 .../seatunnel/e2e/connector/snmp/SnmpSinkIT.java   | 109 +++++++
 .../src/test/resources/fake_to_snmp.conf           |  50 ++++
 seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml   |   1 +
 36 files changed, 2384 insertions(+), 175 deletions(-)

diff --git a/.github/workflows/labeler/label-scope-conf.yml 
b/.github/workflows/labeler/label-scope-conf.yml
index 00c7cf4609..141c3f8469 100644
--- a/.github/workflows/labeler/label-scope-conf.yml
+++ b/.github/workflows/labeler/label-scope-conf.yml
@@ -179,6 +179,13 @@ google-pubsub:
       - changed-files:
           - any-glob-to-any-file: 
seatunnel-connectors-v2/connector-google-pubsub/**
           - all-globs-to-all-files: 
'!seatunnel-connectors-v2/connector-!(google-pubsub)/**'
+snmp:
+  - all:
+      - changed-files:
+          - any-glob-to-any-file:
+              - seatunnel-connectors-v2/connector-snmp/**
+              - seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/**
+          - all-globs-to-all-files: 
'!seatunnel-connectors-v2/connector-!(snmp)/**'
 azure-queue-storage:
   - all:
       - changed-files:
diff --git a/docs/en/connectors/changelog/connector-snmp.md 
b/docs/en/connectors/changelog/connector-snmp.md
index f70bbecaab..832e97161d 100644
--- a/docs/en/connectors/changelog/connector-snmp.md
+++ b/docs/en/connectors/changelog/connector-snmp.md
@@ -3,5 +3,6 @@
 | Change | Commit | Version |
 | --- | --- | --- |
 | [Feature][Connector-V2] Add SNMPv2c polling source connector | - | Next |
+| [Feature][Connector-V2] Add SNMPv2c SET sink connector | - | Next |
 
 </details>
diff --git a/docs/en/connectors/sink/SNMP.md b/docs/en/connectors/sink/SNMP.md
new file mode 100644
index 0000000000..09069855ab
--- /dev/null
+++ b/docs/en/connectors/sink/SNMP.md
@@ -0,0 +1,137 @@
+import ChangeLog from '../changelog/connector-snmp.md';
+
+# SNMP
+
+> SNMPv2c SET sink connector
+
+## Description
+
+The SNMP sink writes each input row to one SNMP agent by sending one 
synchronous SNMPv2c SET request.
+The V1 scope is deliberately limited to SET operations. It does not send traps 
or informs, and it does not support SNMPv1 or SNMPv3.
+
+Every row supplies a numeric OID, a string value, and an SMI value type. The 
corresponding field names are configurable.
+The default mapping consumes the `oid`, `value`, and `value_type` fields 
emitted by the SNMP source; additional fields such as
+`agent` and `poll_time` are ignored by the sink.
+
+## Support Those Engines
+
+> Spark<br/>
+> Flink<br/>
+> SeaTunnel Zeta<br/>
+
+## Key Features
+
+- [x] [batch](../../introduction/concepts/connector-v2-features.md)
+- [x] [stream](../../introduction/concepts/connector-v2-features.md)
+- [x] [parallelism](../../introduction/concepts/connector-v2-features.md)
+
+## Supported DataSource Info
+
+The connector uses SNMP4J and supports SNMPv2c agents reachable over UDP.
+
+| Datasource | Supported Versions | Dependency |
+|------------|--------------------|------------|
+| SNMP agent | SNMPv2c            | 
[Download](https://mvnrepository.com/artifact/org.apache.seatunnel/connector-snmp)
 |
+
+## Sink Options
+
+| Name             | Type   | Required | Default      | Description |
+|------------------|--------|----------|--------------|-------------|
+| host             | String | Yes      | -            | SNMP agent host name 
or IP address. Do not include a protocol or port. |
+| port             | Int    | No       | 161          | SNMP agent UDP port. |
+| community        | String | Yes      | -            | SNMPv2c community 
credential. The connector does not write this value to its logs or errors. |
+| timeout_millis   | Long   | No       | 5000         | Timeout in 
milliseconds for each SET request attempt. |
+| retries          | Int    | No       | 1            | Number of retries 
after the initial SET request attempt. A value of `0` sends one attempt. |
+| oid_field        | String | No       | oid          | Input `STRING` field 
containing the numeric OID to set. |
+| value_field      | String | No       | value        | Input `STRING` field 
containing the value to set. |
+| value_type_field | String | No       | value_type   | Input `STRING` field 
containing the SMI value type. |
+| common-options   |        | No       | -            | [Common Sink 
Options](../common-options/sink-common-options.md), including `plugin_input`. |
+
+Factory option validation checks nonblank strings, the port range, a positive 
timeout, and nonnegative retries before sink construction.
+The three mapped fields must exist in the input schema, must use `STRING`, and 
must refer to distinct fields. Schema errors are
+rejected while the job is created. Null values and blank OID or value-type 
fields are rejected before a network request is sent. The value field is 
validated according to its SMI type; an empty `OctetString` or `OctetStringHex` 
is valid, and text `OctetString` whitespace is preserved.
+
+## Supported SMI Value Types
+
+The `value_type` comparison is case-insensitive and ignores whitespace, `_`, 
and `-` characters.
+The sink accepts both the documented names and SNMP4J syntax strings emitted 
by the SNMP source,
+including `Counter`, `Gauge`, `OCTET STRING`, and `OBJECT IDENTIFIER`.
+
+| Value type | Accepted value |
+|------------|----------------|
+| `Integer32` or `Integer` | Signed 32-bit decimal integer. |
+| `UnsignedInteger32` or `UnsignedInteger` | Decimal integer from 0 through 
4294967295. |
+| `Counter32` or `Counter` | Decimal integer from 0 through 4294967295. |
+| `Gauge32` or `Gauge` | Decimal integer from 0 through 4294967295. |
+| `TimeTicks` | Decimal count of hundredths of a second from 0 through 
4294967295, or the SNMP4J source format `[days, ]hours:mm:ss.hh`. |
+| `Counter64` | Decimal integer from 0 through 18446744073709551615. |
+| `OctetString` or `OCTET STRING` | UTF-8 text represented by the input 
string. |
+| `OctetStringHex` | An even number of hexadecimal characters, such as 
`00ff10`. |
+| `OID` or `OBJECT IDENTIFIER` | Numeric object identifier. Leading dots are 
accepted. |
+| `IpAddress` | Dotted IPv4 address. |
+
+`OctetString` is a textual mapping. Use `OctetStringHex` when byte-for-byte 
binary content is required.
+
+## Example
+
+```hocon
+env {
+  parallelism = 1
+  job.mode = "BATCH"
+}
+
+source {
+  FakeSource {
+    plugin_output = "snmp_updates"
+    schema = {
+      fields {
+        oid = string
+        value = string
+        value_type = string
+      }
+    }
+    rows = [
+      {
+        kind = INSERT
+        fields = {
+          oid = "1.3.6.1.2.1.1.5.0"
+          value = "router-1"
+          value_type = "OctetString"
+        }
+      }
+    ]
+  }
+}
+
+sink {
+  SNMP {
+    plugin_input = "snmp_updates"
+    host = "192.0.2.10"
+    port = 161
+    community = "replace-with-your-community"
+    timeout_millis = 3000
+    retries = 1
+  }
+}
+```
+
+Replace the placeholder community before running the example. Supply the real 
credential outside checked-in job files.
+`community` is automatically masked when the parsed job configuration is 
logged. There is no need to add it to
+`shade.options` for log masking; that option also participates in 
configuration shading/encryption.
+
+## Delivery, Failure, and Security Behavior
+
+- One successful `write` call means the agent returned a successful SNMP 
response for that row.
+- A timeout after all configured attempts or a non-zero SNMP response error 
status fails the sink task.
+- A row can block for approximately `timeout_millis * (retries + 1)` before it 
fails. Keep this below the job's checkpoint timeout.
+- SNMP4J retransmits a timed-out request. A late response can therefore make a 
non-idempotent OID observe the same SET more than once.
+- The sink has no transactional commit protocol or recoverable writer state. 
Engine recovery can repeat a SET request, so delivery is at-least-once.
+- Parallel writers can update the same OID out of order. Use parallelism 1 
when update order matters.
+- Row kinds are not interpreted as CDC operations. Every input row, including 
update or delete row kinds, is treated as a SET request.
+- Treat `community` as a credential. Supply it through configuration 
substitution or another secret-management path, and do not place a real value 
in job files committed to source control.
+- SNMPv2c provides no wire encryption or integrity protection. The community 
and SET payload are sent in cleartext; use only a trusted private network or a 
protected tunnel such as a VPN.
+- Traps, informs, SNMPv1, and SNMPv3 are outside this V1 contract.
+
+See [Common Sink Options](../common-options/sink-common-options.md) for 
options such as `plugin_input`.
+
+<ChangeLog />
diff --git a/docs/zh/connectors/changelog/connector-snmp.md 
b/docs/zh/connectors/changelog/connector-snmp.md
index f70bbecaab..832e97161d 100644
--- a/docs/zh/connectors/changelog/connector-snmp.md
+++ b/docs/zh/connectors/changelog/connector-snmp.md
@@ -3,5 +3,6 @@
 | Change | Commit | Version |
 | --- | --- | --- |
 | [Feature][Connector-V2] Add SNMPv2c polling source connector | - | Next |
+| [Feature][Connector-V2] Add SNMPv2c SET sink connector | - | Next |
 
 </details>
diff --git a/docs/zh/connectors/sink/SNMP.md b/docs/zh/connectors/sink/SNMP.md
new file mode 100644
index 0000000000..f23b0b9702
--- /dev/null
+++ b/docs/zh/connectors/sink/SNMP.md
@@ -0,0 +1,135 @@
+import ChangeLog from '../changelog/connector-snmp.md';
+
+# SNMP
+
+> SNMPv2c SET Sink 连接器
+
+## 描述
+
+SNMP Sink 为每一行输入向一个 SNMP Agent 发送一次同步 SNMPv2c SET 请求。
+V1 范围仅包括 SET 操作,不发送 Trap 或 Inform,也不支持 SNMPv1 或 SNMPv3。
+
+每一行需要提供数字 OID、字符串值和 SMI 值类型,对应的字段名可以配置。
+默认映射会读取 SNMP Source 输出的 `oid`、`value` 和 `value_type` 字段;Sink 会忽略 
`agent`、`poll_time` 等额外字段。
+
+## 支持的引擎
+
+> Spark<br/>
+> Flink<br/>
+> SeaTunnel Zeta<br/>
+
+## 主要特性
+
+- [x] [批处理](../../introduction/concepts/connector-v2-features.md)
+- [x] [流处理](../../introduction/concepts/connector-v2-features.md)
+- [x] [并行度](../../introduction/concepts/connector-v2-features.md)
+
+## 支持的数据源信息
+
+连接器使用 SNMP4J,支持通过 UDP 访问的 SNMPv2c Agent。
+
+| 数据源 | 支持版本 | 依赖 |
+|--------|----------|------|
+| SNMP Agent | SNMPv2c | 
[下载](https://mvnrepository.com/artifact/org.apache.seatunnel/connector-snmp) |
+
+## Sink 配置项
+
+| 名称             | 类型   | 是否必填 | 默认值     | 描述 |
+|------------------|--------|----------|------------|------|
+| host             | String | 是       | -          | SNMP Agent 主机名或 IP 
地址,不要包含协议或端口。 |
+| port             | Int    | 否       | 161        | SNMP Agent 的 UDP 端口。 |
+| community        | String | 是       | -          | SNMPv2c community 
凭证。连接器不会把该值写入日志或错误信息。 |
+| timeout_millis   | Long   | 否       | 5000       | 每次 SET 请求尝试的超时时间,单位为毫秒。 |
+| retries          | Int    | 否       | 1          | 首次 SET 请求失败后的重试次数。`0` 
表示只发送一次。 |
+| oid_field        | String | 否       | oid        | 包含待设置数字 OID 的输入 `STRING` 
字段。 |
+| value_field      | String | 否       | value      | 包含待设置值的输入 `STRING` 字段。 |
+| value_type_field | String | 否       | value_type | 包含 SMI 值类型的输入 `STRING` 
字段。 |
+| common-options   |        | 否       | -          | [通用 Sink 
配置项](../common-options/sink-common-options.md),包括 `plugin_input`。 |
+
+工厂配置校验会在构造 Sink 之前检查字符串非空、端口范围、超时时间为正数以及重试次数非负。
+三个映射字段必须存在于输入 Schema 中、类型必须为 `STRING`,并且不能指向同一个字段。Schema 错误会在创建任务时被拒绝。
+空值以及空白的 OID 或值类型字段会在发送网络请求前被拒绝。值字段会根据其 SMI 类型进行校验;空的 `OctetString` 或 
`OctetStringHex` 是有效值,文本 `OctetString` 的前后空白会被保留。
+
+## 支持的 SMI 值类型
+
+`value_type` 不区分大小写,并忽略空白、`_` 和 `-` 字符。
+Sink 同时接受文档中的类型名和 SNMP Source 输出的 SNMP4J 语法字符串,包括 `Counter`、`Gauge`、`OCTET 
STRING` 和 `OBJECT IDENTIFIER`。
+
+| 值类型 | 可接受的值 |
+|--------|------------|
+| `Integer32` 或 `Integer` | 有符号 32 位十进制整数。 |
+| `UnsignedInteger32` 或 `UnsignedInteger` | 0 到 4294967295 的十进制整数。 |
+| `Counter32` 或 `Counter` | 0 到 4294967295 的十进制整数。 |
+| `Gauge32` 或 `Gauge` | 0 到 4294967295 的十进制整数。 |
+| `TimeTicks` | 0 到 4294967295 的十进制百分之一秒计数,或 SNMP Source 使用的 SNMP4J 格式 `[days, 
]hours:mm:ss.hh`。 |
+| `Counter64` | 0 到 18446744073709551615 的十进制整数。 |
+| `OctetString` 或 `OCTET STRING` | 输入字符串表示的 UTF-8 文本。 |
+| `OctetStringHex` | 偶数个十六进制字符,例如 `00ff10`。 |
+| `OID` 或 `OBJECT IDENTIFIER` | 数字对象标识符,可以带前导点。 |
+| `IpAddress` | 点分 IPv4 地址。 |
+
+`OctetString` 用于文本映射。如果需要逐字节保存二进制内容,请使用 `OctetStringHex`。
+
+## 示例
+
+```hocon
+env {
+  parallelism = 1
+  job.mode = "BATCH"
+}
+
+source {
+  FakeSource {
+    plugin_output = "snmp_updates"
+    schema = {
+      fields {
+        oid = string
+        value = string
+        value_type = string
+      }
+    }
+    rows = [
+      {
+        kind = INSERT
+        fields = {
+          oid = "1.3.6.1.2.1.1.5.0"
+          value = "router-1"
+          value_type = "OctetString"
+        }
+      }
+    ]
+  }
+}
+
+sink {
+  SNMP {
+    plugin_input = "snmp_updates"
+    host = "192.0.2.10"
+    port = 161
+    community = "replace-with-your-community"
+    timeout_millis = 3000
+    retries = 1
+  }
+}
+```
+
+运行示例前请替换 community 占位值,并在已提交到源码的任务文件之外提供真实凭据。
+记录解析后的任务配置时,`community` 会自动脱敏。无需为日志脱敏将其加入 `shade.options`;
+该选项还参与配置的遮蔽和加密流程。
+
+## 投递、失败和安全语义
+
+- 一次 `write` 成功表示 Agent 已对该行返回成功的 SNMP 响应。
+- 所有配置尝试完成后仍超时,或 SNMP 响应包含非零错误状态时,Sink Task 会失败。
+- 一行在失败前可能阻塞约 `timeout_millis * (retries + 1)`。请确保该时间小于任务的 Checkpoint 超时时间。
+- SNMP4J 会重发超时请求。迟到的响应可能导致非幂等 OID 多次观察到同一次 SET。
+- Sink 没有事务提交协议或可恢复的 Writer 状态。引擎恢复后可能重复发送 SET,因此投递语义为至少一次。
+- 多个并行 Writer 可能乱序更新同一个 OID。如果更新顺序很重要,请使用并行度 1。
+- Sink 不会把 RowKind 解释为 CDC 操作。所有输入行(包括更新或删除类型)都会作为 SET 请求处理。
+- 请把 `community` 视为凭证,通过配置替换或其他密钥管理方式提供,不要把真实值提交到源码中的任务文件。
+- SNMPv2c 不提供传输加密或完整性保护,community 和 SET 负载会以明文发送。请仅在可信私有网络中使用,或通过 VPN 等受保护隧道传输。
+- Trap、Inform、SNMPv1 和 SNMPv3 不属于 V1 范围。
+
+`plugin_input` 等配置请参阅[通用 Sink 配置项](../common-options/sink-common-options.md)。
+
+<ChangeLog />
diff --git a/plugin-mapping.properties b/plugin-mapping.properties
index 6baf75df38..1ac16a0fc5 100644
--- a/plugin-mapping.properties
+++ b/plugin-mapping.properties
@@ -164,6 +164,7 @@ seatunnel.sink.ActiveMQ = connector-activemq
 seatunnel.source.MQTT = connector-mqtt
 seatunnel.sink.MQTT = connector-mqtt
 seatunnel.source.SNMP = connector-snmp
+seatunnel.sink.SNMP = connector-snmp
 seatunnel.source.Python = connector-python
 seatunnel.source.Prometheus = connector-prometheus
 seatunnel.sink.Prometheus = connector-prometheus
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/client/SnmpTargetFactory.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/client/SnmpTargetFactory.java
new file mode 100644
index 0000000000..4d53a4249d
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/client/SnmpTargetFactory.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.client;
+
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpTargetConfig;
+import 
org.apache.seatunnel.connectors.seatunnel.snmp.exception.SnmpConnectorErrorCode;
+import 
org.apache.seatunnel.connectors.seatunnel.snmp.exception.SnmpConnectorException;
+
+import org.snmp4j.CommunityTarget;
+import org.snmp4j.Target;
+import org.snmp4j.mp.SnmpConstants;
+import org.snmp4j.smi.OctetString;
+import org.snmp4j.smi.UdpAddress;
+
+/** Builds SNMP4J targets without exposing credentials through logs or error 
messages. */
+public final class SnmpTargetFactory {
+
+    private SnmpTargetFactory() {}
+
+    public static Target create(SnmpTargetConfig config) {
+        CommunityTarget target = new CommunityTarget();
+        try {
+            target.setAddress(new UdpAddress(config.getHost() + "/" + 
config.getPort()));
+        } catch (IllegalArgumentException e) {
+            throw new SnmpConnectorException(
+                    SnmpConnectorErrorCode.INVALID_CONFIG,
+                    "Invalid SNMP agent address " + config.getHost() + ":" + 
config.getPort(),
+                    e);
+        }
+        target.setCommunity(new OctetString(config.getCommunity()));
+        target.setVersion(SnmpConstants.version2c);
+        target.setTimeout(config.getTimeoutMillis());
+        target.setRetries(config.getRetries());
+        return target;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceOptions.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpOptions.java
similarity index 78%
copy from 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceOptions.java
copy to 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpOptions.java
index c309801f32..dc17c22c4f 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceOptions.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpOptions.java
@@ -20,9 +20,8 @@ package org.apache.seatunnel.connectors.seatunnel.snmp.config;
 import org.apache.seatunnel.api.configuration.Option;
 import org.apache.seatunnel.api.configuration.Options;
 
-import java.util.List;
-
-public final class SnmpSourceOptions {
+/** Options shared by SNMP source and sink connections. */
+public final class SnmpOptions {
 
     public static final String CONNECTOR_IDENTITY = "SNMP";
 
@@ -41,12 +40,6 @@ public final class SnmpSourceOptions {
                     .noDefaultValue()
                     .withDescription("SNMPv2c community credential");
 
-    public static final Option<List<String>> OIDS =
-            Options.key("oids")
-                    .listType()
-                    .noDefaultValue()
-                    .withDescription("Numeric OIDs to retrieve with SNMP GET");
-
     public static final Option<Long> TIMEOUT_MILLIS =
             Options.key("timeout_millis")
                     .longType()
@@ -59,11 +52,5 @@ public final class SnmpSourceOptions {
                     .defaultValue(1)
                     .withDescription("Number of retries after the initial SNMP 
request attempt");
 
-    public static final Option<Long> POLL_INTERVAL_MILLIS =
-            Options.key("poll_interval_millis")
-                    .longType()
-                    .defaultValue(60000L)
-                    .withDescription("Interval in milliseconds between 
streaming polls");
-
-    private SnmpSourceOptions() {}
+    private SnmpOptions() {}
 }
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSinkConfig.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSinkConfig.java
new file mode 100644
index 0000000000..3efa98f417
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSinkConfig.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.config;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Validated runtime configuration for the SNMPv2c SET sink. */
+public final class SnmpSinkConfig implements Serializable, SnmpTargetConfig {
+
+    private static final long serialVersionUID = 1L;
+
+    private final String host;
+    private final int port;
+    private final String community;
+    private final long timeoutMillis;
+    private final int retries;
+    private final String oidField;
+    private final String valueField;
+    private final String valueTypeField;
+
+    public SnmpSinkConfig(ReadonlyConfig config) {
+        String configuredHost = config.get(SnmpSinkOptions.HOST);
+        if (isBlank(configuredHost)) {
+            throw new IllegalArgumentException("SNMP sink host must not be 
blank");
+        }
+        this.host = configuredHost.trim();
+        this.port = config.get(SnmpSinkOptions.PORT);
+        this.community = config.get(SnmpSinkOptions.COMMUNITY);
+        this.timeoutMillis = config.get(SnmpSinkOptions.TIMEOUT_MILLIS);
+        this.retries = config.get(SnmpSinkOptions.RETRIES);
+        this.oidField = requireField(config.get(SnmpSinkOptions.OID_FIELD), 
"oid_field");
+        this.valueField = 
requireField(config.get(SnmpSinkOptions.VALUE_FIELD), "value_field");
+        this.valueTypeField =
+                requireField(config.get(SnmpSinkOptions.VALUE_TYPE_FIELD), 
"value_type_field");
+        validateTarget();
+        validateDistinctFields();
+    }
+
+    private void validateTarget() {
+        if (port < 1 || port > 65535) {
+            throw new IllegalArgumentException("SNMP sink port must be between 
1 and 65535");
+        }
+        if (isBlank(community)) {
+            throw new IllegalArgumentException("SNMP sink community must not 
be blank");
+        }
+        if (timeoutMillis <= 0) {
+            throw new IllegalArgumentException("SNMP sink timeout_millis must 
be greater than 0");
+        }
+        if (retries < 0) {
+            throw new IllegalArgumentException("SNMP sink retries must not be 
negative");
+        }
+    }
+
+    private static String requireField(String configuredField, String 
optionName) {
+        if (isBlank(configuredField)) {
+            throw new IllegalArgumentException("SNMP sink " + optionName + " 
must not be blank");
+        }
+        return configuredField.trim();
+    }
+
+    private static boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+
+    private void validateDistinctFields() {
+        Set<String> fields = new HashSet<>();
+        fields.add(oidField);
+        fields.add(valueField);
+        fields.add(valueTypeField);
+        if (fields.size() != 3) {
+            throw new IllegalArgumentException(
+                    "SNMP sink oid_field, value_field, and value_type_field 
must be distinct");
+        }
+    }
+
+    @Override
+    public String getHost() {
+        return host;
+    }
+
+    @Override
+    public int getPort() {
+        return port;
+    }
+
+    @Override
+    public String getCommunity() {
+        return community;
+    }
+
+    @Override
+    public long getTimeoutMillis() {
+        return timeoutMillis;
+    }
+
+    @Override
+    public int getRetries() {
+        return retries;
+    }
+
+    public String getOidField() {
+        return oidField;
+    }
+
+    public String getValueField() {
+        return valueField;
+    }
+
+    public String getValueTypeField() {
+        return valueTypeField;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSinkOptions.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSinkOptions.java
new file mode 100644
index 0000000000..7034675e26
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSinkOptions.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.config;
+
+import org.apache.seatunnel.api.configuration.Option;
+import org.apache.seatunnel.api.configuration.Options;
+
+/** Configuration options for the SNMPv2c SET sink. */
+public final class SnmpSinkOptions {
+
+    public static final String CONNECTOR_IDENTITY = 
SnmpOptions.CONNECTOR_IDENTITY;
+
+    public static final Option<String> HOST = SnmpOptions.HOST;
+    public static final Option<Integer> PORT = SnmpOptions.PORT;
+    public static final Option<String> COMMUNITY = SnmpOptions.COMMUNITY;
+    public static final Option<Long> TIMEOUT_MILLIS = 
SnmpOptions.TIMEOUT_MILLIS;
+    public static final Option<Integer> RETRIES = SnmpOptions.RETRIES;
+
+    public static final Option<String> OID_FIELD =
+            Options.key("oid_field")
+                    .stringType()
+                    .defaultValue("oid")
+                    .withDescription("Input STRING field containing the 
numeric OID to set");
+
+    public static final Option<String> VALUE_FIELD =
+            Options.key("value_field")
+                    .stringType()
+                    .defaultValue("value")
+                    .withDescription("Input STRING field containing the value 
to set");
+
+    public static final Option<String> VALUE_TYPE_FIELD =
+            Options.key("value_type_field")
+                    .stringType()
+                    .defaultValue("value_type")
+                    .withDescription("Input STRING field containing the SMI 
value type");
+
+    private SnmpSinkOptions() {}
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceConfig.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceConfig.java
index 09065192e5..affb3da361 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceConfig.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceConfig.java
@@ -30,7 +30,7 @@ import java.util.Set;
 import java.util.regex.Pattern;
 
 /** Validated runtime configuration for the SNMP source connector. */
-public final class SnmpSourceConfig implements Serializable {
+public final class SnmpSourceConfig implements Serializable, SnmpTargetConfig {
 
     private static final long serialVersionUID = 1L;
 
@@ -79,6 +79,25 @@ public final class SnmpSourceConfig implements Serializable {
         }
     }
 
+    private static boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+
+    @Override
+    public String getHost() {
+        return host;
+    }
+
+    @Override
+    public int getPort() {
+        return port;
+    }
+
+    @Override
+    public String getCommunity() {
+        return community;
+    }
+
     private static List<OID> parseOids(List<String> configuredOids) {
         if (configuredOids == null || configuredOids.isEmpty()) {
             throw new IllegalArgumentException("SNMP source oids must not be 
empty");
@@ -110,30 +129,16 @@ public final class SnmpSourceConfig implements 
Serializable {
         return Collections.unmodifiableList(parsed);
     }
 
-    private static boolean isBlank(String value) {
-        return value == null || value.trim().isEmpty();
-    }
-
-    public String getHost() {
-        return host;
-    }
-
-    public int getPort() {
-        return port;
-    }
-
-    public String getCommunity() {
-        return community;
-    }
-
     public List<OID> getOids() {
         return oids;
     }
 
+    @Override
     public long getTimeoutMillis() {
         return timeoutMillis;
     }
 
+    @Override
     public int getRetries() {
         return retries;
     }
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceOptions.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceOptions.java
index c309801f32..501e5fc6b7 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceOptions.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceOptions.java
@@ -24,22 +24,13 @@ import java.util.List;
 
 public final class SnmpSourceOptions {
 
-    public static final String CONNECTOR_IDENTITY = "SNMP";
+    public static final String CONNECTOR_IDENTITY = 
SnmpOptions.CONNECTOR_IDENTITY;
 
-    public static final Option<String> HOST =
-            Options.key("host")
-                    .stringType()
-                    .noDefaultValue()
-                    .withDescription("SNMP agent host name or IP address");
+    public static final Option<String> HOST = SnmpOptions.HOST;
 
-    public static final Option<Integer> PORT =
-            
Options.key("port").intType().defaultValue(161).withDescription("SNMP agent UDP 
port");
+    public static final Option<Integer> PORT = SnmpOptions.PORT;
 
-    public static final Option<String> COMMUNITY =
-            Options.key("community")
-                    .stringType()
-                    .noDefaultValue()
-                    .withDescription("SNMPv2c community credential");
+    public static final Option<String> COMMUNITY = SnmpOptions.COMMUNITY;
 
     public static final Option<List<String>> OIDS =
             Options.key("oids")
@@ -47,17 +38,9 @@ public final class SnmpSourceOptions {
                     .noDefaultValue()
                     .withDescription("Numeric OIDs to retrieve with SNMP GET");
 
-    public static final Option<Long> TIMEOUT_MILLIS =
-            Options.key("timeout_millis")
-                    .longType()
-                    .defaultValue(5000L)
-                    .withDescription("Timeout in milliseconds for each SNMP 
request attempt");
+    public static final Option<Long> TIMEOUT_MILLIS = 
SnmpOptions.TIMEOUT_MILLIS;
 
-    public static final Option<Integer> RETRIES =
-            Options.key("retries")
-                    .intType()
-                    .defaultValue(1)
-                    .withDescription("Number of retries after the initial SNMP 
request attempt");
+    public static final Option<Integer> RETRIES = SnmpOptions.RETRIES;
 
     public static final Option<Long> POLL_INTERVAL_MILLIS =
             Options.key("poll_interval_millis")
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpTargetConfig.java
similarity index 54%
copy from 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
copy to 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpTargetConfig.java
index bb87272872..e785652053 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpTargetConfig.java
@@ -15,29 +15,20 @@
  * limitations under the License.
  */
 
-package org.apache.seatunnel.connectors.seatunnel.snmp.exception;
+package org.apache.seatunnel.connectors.seatunnel.snmp.config;
 
-import org.apache.seatunnel.common.exception.SeaTunnelErrorCode;
+import java.io.Serializable;
 
-public enum SnmpConnectorErrorCode implements SeaTunnelErrorCode {
-    CONNECTION_FAILED("SNMP-01", "SNMP client initialization failed"),
-    POLL_FAILED("SNMP-02", "SNMP poll failed");
+/** Serializable target settings shared by SNMP source and sink clients. */
+public interface SnmpTargetConfig extends Serializable {
 
-    private final String code;
-    private final String description;
+    String getHost();
 
-    SnmpConnectorErrorCode(String code, String description) {
-        this.code = code;
-        this.description = description;
-    }
+    int getPort();
 
-    @Override
-    public String getCode() {
-        return code;
-    }
+    String getCommunity();
 
-    @Override
-    public String getDescription() {
-        return description;
-    }
+    long getTimeoutMillis();
+
+    int getRetries();
 }
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
index bb87272872..d660e34246 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
@@ -21,7 +21,11 @@ import 
org.apache.seatunnel.common.exception.SeaTunnelErrorCode;
 
 public enum SnmpConnectorErrorCode implements SeaTunnelErrorCode {
     CONNECTION_FAILED("SNMP-01", "SNMP client initialization failed"),
-    POLL_FAILED("SNMP-02", "SNMP poll failed");
+    POLL_FAILED("SNMP-02", "SNMP poll failed"),
+    WRITE_FAILED("SNMP-03", "SNMP SET request failed"),
+    INVALID_CONFIG("SNMP-04", "Invalid SNMP connector configuration"),
+    INVALID_ROW("SNMP-05", "Invalid SNMP sink row"),
+    CLOSE_FAILED("SNMP-06", "SNMP client close failed");
 
     private final String code;
     private final String description;
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/source/Snmp4jClient.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/Snmp4jSetClient.java
similarity index 53%
copy from 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/source/Snmp4jClient.java
copy to 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/Snmp4jSetClient.java
index f4095e9fda..f297194425 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/source/Snmp4jClient.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/Snmp4jSetClient.java
@@ -15,44 +15,38 @@
  * limitations under the License.
  */
 
-package org.apache.seatunnel.connectors.seatunnel.snmp.source;
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
 
-import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSourceConfig;
+import org.apache.seatunnel.connectors.seatunnel.snmp.client.SnmpTargetFactory;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkConfig;
 
-import org.snmp4j.CommunityTarget;
 import org.snmp4j.PDU;
 import org.snmp4j.Snmp;
 import org.snmp4j.Target;
 import org.snmp4j.event.ResponseEvent;
-import org.snmp4j.mp.SnmpConstants;
-import org.snmp4j.smi.OID;
-import org.snmp4j.smi.OctetString;
-import org.snmp4j.smi.UdpAddress;
-import org.snmp4j.smi.Variable;
 import org.snmp4j.smi.VariableBinding;
 import org.snmp4j.transport.DefaultUdpTransportMapping;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 
-/** SNMPv2c client backed by SNMP4J. */
-final class Snmp4jClient implements SnmpClient {
+/** SNMPv2c SET client backed by SNMP4J. */
+final class Snmp4jSetClient implements SnmpSetClient {
 
-    private final SnmpSourceConfig config;
+    private final SnmpSinkConfig config;
     private final Snmp snmp;
     private final Target target;
 
-    Snmp4jClient(SnmpSourceConfig config) throws IOException {
+    Snmp4jSetClient(SnmpSinkConfig config) throws IOException {
         this(config, new Snmp(new DefaultUdpTransportMapping()));
     }
 
-    Snmp4jClient(SnmpSourceConfig config, Snmp snmp) throws IOException {
+    Snmp4jSetClient(SnmpSinkConfig config, Snmp snmp) throws IOException {
         this.config = config;
-        this.target = buildTarget(config);
+        Target createdTarget;
         try {
+            createdTarget = SnmpTargetFactory.create(config);
             snmp.listen();
-        } catch (IOException e) {
+        } catch (IOException | RuntimeException e) {
             try {
                 snmp.close();
             } catch (IOException closeException) {
@@ -60,15 +54,16 @@ final class Snmp4jClient implements SnmpClient {
             }
             throw e;
         }
+        this.target = createdTarget;
         this.snmp = snmp;
     }
 
     @Override
-    public List<SnmpRecord> get(List<OID> oids) throws IOException {
-        ResponseEvent event = snmp.send(buildGetRequest(oids), target);
+    public void set(SnmpSetRequest request) throws IOException {
+        ResponseEvent event = snmp.send(buildSetRequest(request), target);
         if (event == null || event.getResponse() == null) {
             throw new IOException(
-                    "SNMP request timed out for agent "
+                    "SNMP SET request timed out for agent "
                             + config.getHost()
                             + ":"
                             + config.getPort());
@@ -84,7 +79,6 @@ final class Snmp4jClient implements SnmpClient {
                             + ") at index "
                             + response.getErrorIndex());
         }
-        return extractRecords(response);
     }
 
     @Override
@@ -92,35 +86,10 @@ final class Snmp4jClient implements SnmpClient {
         snmp.close();
     }
 
-    static PDU buildGetRequest(List<OID> oids) {
+    static PDU buildSetRequest(SnmpSetRequest request) {
         PDU pdu = new PDU();
-        pdu.setType(PDU.GET);
-        for (OID oid : oids) {
-            pdu.add(new VariableBinding(oid));
-        }
+        pdu.setType(PDU.SET);
+        pdu.add(new VariableBinding(request.getOid(), request.getValue()));
         return pdu;
     }
-
-    static Target buildTarget(SnmpSourceConfig config) {
-        CommunityTarget target = new CommunityTarget();
-        target.setAddress(new UdpAddress(config.getHost() + "/" + 
config.getPort()));
-        target.setCommunity(new OctetString(config.getCommunity()));
-        target.setVersion(SnmpConstants.version2c);
-        target.setTimeout(config.getTimeoutMillis());
-        target.setRetries(config.getRetries());
-        return target;
-    }
-
-    static List<SnmpRecord> extractRecords(PDU response) {
-        List<SnmpRecord> records = new ArrayList<>(response.size());
-        for (VariableBinding binding : response.getVariableBindings()) {
-            Variable variable = binding.getVariable();
-            records.add(
-                    new SnmpRecord(
-                            binding.getOid().toString(),
-                            variable.toString(),
-                            variable.getSyntaxString()));
-        }
-        return records;
-    }
 }
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSetClient.java
similarity index 54%
copy from 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
copy to 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSetClient.java
index bb87272872..5d0e40c4e4 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSetClient.java
@@ -15,29 +15,12 @@
  * limitations under the License.
  */
 
-package org.apache.seatunnel.connectors.seatunnel.snmp.exception;
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
 
-import org.apache.seatunnel.common.exception.SeaTunnelErrorCode;
+import java.io.Closeable;
+import java.io.IOException;
 
-public enum SnmpConnectorErrorCode implements SeaTunnelErrorCode {
-    CONNECTION_FAILED("SNMP-01", "SNMP client initialization failed"),
-    POLL_FAILED("SNMP-02", "SNMP poll failed");
+interface SnmpSetClient extends Closeable {
 
-    private final String code;
-    private final String description;
-
-    SnmpConnectorErrorCode(String code, String description) {
-        this.code = code;
-        this.description = description;
-    }
-
-    @Override
-    public String getCode() {
-        return code;
-    }
-
-    @Override
-    public String getDescription() {
-        return description;
-    }
+    void set(SnmpSetRequest request) throws IOException;
 }
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSetRequest.java
similarity index 55%
copy from 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
copy to 
seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSetRequest.java
index bb87272872..ce8070b365 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/exception/SnmpConnectorErrorCode.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSetRequest.java
@@ -15,29 +15,26 @@
  * limitations under the License.
  */
 
-package org.apache.seatunnel.connectors.seatunnel.snmp.exception;
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
 
-import org.apache.seatunnel.common.exception.SeaTunnelErrorCode;
+import org.snmp4j.smi.OID;
+import org.snmp4j.smi.Variable;
 
-public enum SnmpConnectorErrorCode implements SeaTunnelErrorCode {
-    CONNECTION_FAILED("SNMP-01", "SNMP client initialization failed"),
-    POLL_FAILED("SNMP-02", "SNMP poll failed");
+final class SnmpSetRequest {
 
-    private final String code;
-    private final String description;
+    private final OID oid;
+    private final Variable value;
 
-    SnmpConnectorErrorCode(String code, String description) {
-        this.code = code;
-        this.description = description;
+    SnmpSetRequest(OID oid, Variable value) {
+        this.oid = oid;
+        this.value = value;
     }
 
-    @Override
-    public String getCode() {
-        return code;
+    OID getOid() {
+        return oid;
     }
 
-    @Override
-    public String getDescription() {
-        return description;
+    Variable getValue() {
+        return value;
     }
 }
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSink.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSink.java
new file mode 100644
index 0000000000..65f4f5a474
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSink.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
+
+import org.apache.seatunnel.api.sink.SinkWriter;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import 
org.apache.seatunnel.connectors.seatunnel.common.sink.AbstractSimpleSink;
+import 
org.apache.seatunnel.connectors.seatunnel.common.sink.AbstractSinkWriter;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkConfig;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkOptions;
+
+import java.util.Optional;
+
+/** SeaTunnel sink that applies input rows through SNMPv2c SET requests. */
+public final class SnmpSink extends AbstractSimpleSink<SeaTunnelRow, Void> {
+
+    private final SnmpSinkConfig config;
+    private final CatalogTable catalogTable;
+    private final SeaTunnelRowType rowType;
+
+    public SnmpSink(SnmpSinkConfig config, CatalogTable catalogTable) {
+        this.config = config;
+        this.catalogTable = catalogTable;
+        this.rowType = catalogTable.getSeaTunnelRowType();
+        new SnmpSinkRowConverter(config, rowType);
+    }
+
+    @Override
+    public String getPluginName() {
+        return SnmpSinkOptions.CONNECTOR_IDENTITY;
+    }
+
+    @Override
+    public AbstractSinkWriter<SeaTunnelRow, Void> 
createWriter(SinkWriter.Context context) {
+        return new SnmpSinkWriter(config, rowType);
+    }
+
+    @Override
+    public Optional<CatalogTable> getWriteCatalogTable() {
+        return Optional.of(catalogTable);
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkFactory.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkFactory.java
new file mode 100644
index 0000000000..f90fc44db5
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkFactory.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
+
+import org.apache.seatunnel.api.configuration.util.Conditions;
+import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.table.connector.TableSink;
+import org.apache.seatunnel.api.table.factory.Factory;
+import org.apache.seatunnel.api.table.factory.TableSinkFactory;
+import org.apache.seatunnel.api.table.factory.TableSinkFactoryContext;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkConfig;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkOptions;
+
+import com.google.auto.service.AutoService;
+
+/** Creates SNMPv2c SET sinks from table factory configuration. */
+@AutoService(Factory.class)
+public final class SnmpSinkFactory implements TableSinkFactory {
+
+    @Override
+    public String factoryIdentifier() {
+        return SnmpSinkOptions.CONNECTOR_IDENTITY;
+    }
+
+    @Override
+    public OptionRule optionRule() {
+        return OptionRule.builder()
+                .required(SnmpSinkOptions.HOST, 
Conditions.notBlank(SnmpSinkOptions.HOST))
+                .required(SnmpSinkOptions.COMMUNITY, 
Conditions.notBlank(SnmpSinkOptions.COMMUNITY))
+                .optional(
+                        SnmpSinkOptions.PORT,
+                        Conditions.greaterOrEqual(SnmpSinkOptions.PORT, 1)
+                                
.and(Conditions.lessOrEqual(SnmpSinkOptions.PORT, 65535)))
+                .optional(
+                        SnmpSinkOptions.TIMEOUT_MILLIS,
+                        Conditions.greaterThan(SnmpSinkOptions.TIMEOUT_MILLIS, 
0L))
+                .optional(
+                        SnmpSinkOptions.RETRIES,
+                        Conditions.greaterOrEqual(SnmpSinkOptions.RETRIES, 0))
+                .optional(SnmpSinkOptions.OID_FIELD, 
Conditions.notBlank(SnmpSinkOptions.OID_FIELD))
+                .optional(
+                        SnmpSinkOptions.VALUE_FIELD,
+                        Conditions.notBlank(SnmpSinkOptions.VALUE_FIELD))
+                .optional(
+                        SnmpSinkOptions.VALUE_TYPE_FIELD,
+                        Conditions.notBlank(SnmpSinkOptions.VALUE_TYPE_FIELD))
+                .build();
+    }
+
+    @Override
+    public TableSink createSink(TableSinkFactoryContext context) {
+        SnmpSinkConfig config = new SnmpSinkConfig(context.getOptions());
+        return () -> new SnmpSink(config, context.getCatalogTable());
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkRowConverter.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkRowConverter.java
new file mode 100644
index 0000000000..3e424ff072
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkRowConverter.java
@@ -0,0 +1,316 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
+
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.api.table.type.SqlType;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkConfig;
+import 
org.apache.seatunnel.connectors.seatunnel.snmp.exception.SnmpConnectorErrorCode;
+import 
org.apache.seatunnel.connectors.seatunnel.snmp.exception.SnmpConnectorException;
+
+import org.snmp4j.smi.Counter32;
+import org.snmp4j.smi.Counter64;
+import org.snmp4j.smi.Gauge32;
+import org.snmp4j.smi.Integer32;
+import org.snmp4j.smi.IpAddress;
+import org.snmp4j.smi.OID;
+import org.snmp4j.smi.OctetString;
+import org.snmp4j.smi.TimeTicks;
+import org.snmp4j.smi.UnsignedInteger32;
+import org.snmp4j.smi.Variable;
+
+import java.io.Serializable;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Converts the configured input fields into one validated SNMP SET binding. 
*/
+final class SnmpSinkRowConverter implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+    private static final long MAX_UNSIGNED_32 = 0xFFFFFFFFL;
+    private static final BigInteger MAX_UNSIGNED_64 = new 
BigInteger("18446744073709551615");
+    private static final Pattern NUMERIC_OID = 
Pattern.compile("^\\.?[0-9]+(\\.[0-9]+)+$");
+    private static final Pattern HEX_VALUE = Pattern.compile("^[0-9a-fA-F]*$");
+    private static final Pattern IPV4_VALUE = 
Pattern.compile("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$");
+    private static final Pattern DECIMAL_INTEGER = 
Pattern.compile("^[+-]?[0-9]+$");
+    private static final Pattern FORMATTED_TIME_TICKS =
+            Pattern.compile(
+                    "^(?:([0-9]+) (day|days), 
)?([0-9]{1,2}):([0-9]{2}):([0-9]{2})\\.([0-9]{2})$");
+    private static final BigInteger TICKS_PER_DAY = 
BigInteger.valueOf(8_640_000L);
+    private static final BigInteger TICKS_PER_HOUR = 
BigInteger.valueOf(360_000L);
+    private static final BigInteger TICKS_PER_MINUTE = 
BigInteger.valueOf(6_000L);
+    private static final BigInteger TICKS_PER_SECOND = 
BigInteger.valueOf(100L);
+
+    private final int rowArity;
+    private final int oidIndex;
+    private final int valueIndex;
+    private final int valueTypeIndex;
+
+    SnmpSinkRowConverter(SnmpSinkConfig config, SeaTunnelRowType rowType) {
+        this.rowArity = rowType.getTotalFields();
+        this.oidIndex = requireStringField(rowType, config.getOidField(), 
"oid_field");
+        this.valueIndex = requireStringField(rowType, config.getValueField(), 
"value_field");
+        this.valueTypeIndex =
+                requireStringField(rowType, config.getValueTypeField(), 
"value_type_field");
+    }
+
+    SnmpSetRequest convert(SeaTunnelRow row) {
+        if (row.getArity() != rowArity) {
+            throw invalidRow(
+                    "Input row arity "
+                            + row.getArity()
+                            + " does not match the configured schema arity "
+                            + rowArity);
+        }
+
+        String oid = requireNonBlankRowValue(row, oidIndex, "OID");
+        String value = requireNonNullRowValue(row, valueIndex, "value");
+        String valueType = requireNonBlankRowValue(row, valueTypeIndex, "value 
type");
+        return new SnmpSetRequest(parseOid(oid), parseVariable(valueType, 
value));
+    }
+
+    private static int requireStringField(
+            SeaTunnelRowType rowType, String fieldName, String optionName) {
+        int index = rowType.indexOf(fieldName, false);
+        if (index < 0) {
+            throw invalidConfig(
+                    "Option `"
+                            + optionName
+                            + "` references unknown field `"
+                            + fieldName
+                            + "`. Available fields are "
+                            + Arrays.toString(rowType.getFieldNames()));
+        }
+        SeaTunnelDataType<?> dataType = rowType.getFieldType(index);
+        if (dataType.getSqlType() != SqlType.STRING) {
+            throw invalidConfig(
+                    "Field `"
+                            + fieldName
+                            + "` configured by `"
+                            + optionName
+                            + "` must use STRING type, but was "
+                            + dataType.getSqlType());
+        }
+        return index;
+    }
+
+    private static String requireNonBlankRowValue(
+            SeaTunnelRow row, int index, String fieldDescription) {
+        String stringValue = requireNonNullRowValue(row, index, 
fieldDescription);
+        if (stringValue.trim().isEmpty()) {
+            throw invalidRow("SNMP sink " + fieldDescription + " field must 
not be blank");
+        }
+        return stringValue.trim();
+    }
+
+    private static String requireNonNullRowValue(
+            SeaTunnelRow row, int index, String fieldDescription) {
+        Object value = row.getField(index);
+        if (value == null) {
+            throw invalidRow("SNMP sink " + fieldDescription + " field must 
not be null");
+        }
+        if (!(value instanceof String)) {
+            throw invalidRow("SNMP sink " + fieldDescription + " field must 
contain a STRING");
+        }
+        return (String) value;
+    }
+
+    static OID parseOid(String configuredOid) {
+        String value = configuredOid.trim();
+        if (!NUMERIC_OID.matcher(value).matches()) {
+            throw invalidRow("SNMP sink OID must be numeric: " + 
configuredOid);
+        }
+        if (value.charAt(0) == '.') {
+            value = value.substring(1);
+        }
+        try {
+            OID oid = new OID(value);
+            if (!oid.isValid()) {
+                throw invalidRow("SNMP sink OID is invalid: " + configuredOid);
+            }
+            return oid;
+        } catch (SnmpConnectorException e) {
+            throw e;
+        } catch (RuntimeException e) {
+            throw invalidRow("SNMP sink OID is invalid: " + configuredOid, e);
+        }
+    }
+
+    static Variable parseVariable(String configuredType, String value) {
+        String normalizedType = normalizeType(configuredType);
+        try {
+            switch (normalizedType) {
+                case "INTEGER":
+                case "INTEGER32":
+                    return new Integer32(Integer.parseInt(value));
+                case "UNSIGNEDINTEGER":
+                case "UNSIGNEDINTEGER32":
+                    return new UnsignedInteger32(parseUnsigned32(value, 
configuredType));
+                case "COUNTER":
+                case "COUNTER32":
+                    return new Counter32(parseUnsigned32(value, 
configuredType));
+                case "GAUGE":
+                case "GAUGE32":
+                    return new Gauge32(parseUnsigned32(value, configuredType));
+                case "TIMETICKS":
+                    return new TimeTicks(parseTimeTicks(value, 
configuredType));
+                case "COUNTER64":
+                    return new Counter64(parseUnsigned64(value, 
configuredType));
+                case "OCTETSTRING":
+                    return new 
OctetString(value.getBytes(StandardCharsets.UTF_8));
+                case "OCTETSTRINGHEX":
+                    return new OctetString(parseHex(value));
+                case "OBJECTIDENTIFIER":
+                case "OID":
+                    return parseOid(value);
+                case "IPADDRESS":
+                    validateIpv4(value);
+                    return new IpAddress(value);
+                default:
+                    throw invalidRow(
+                            "Unsupported SNMP sink value type `"
+                                    + configuredType
+                                    + "`. Supported types are Integer32, 
UnsignedInteger32, "
+                                    + "Counter/Counter32, Gauge/Gauge32, 
TimeTicks, Counter64, "
+                                    + "OctetString/OCTET STRING, 
OctetStringHex, "
+                                    + "OID/OBJECT IDENTIFIER, and IpAddress");
+            }
+        } catch (SnmpConnectorException e) {
+            throw e;
+        } catch (RuntimeException e) {
+            throw invalidRow("SNMP sink value is invalid for type `" + 
configuredType + "`", e);
+        }
+    }
+
+    private static String normalizeType(String configuredType) {
+        StringBuilder normalized = new StringBuilder(configuredType.length());
+        for (int index = 0; index < configuredType.length(); index++) {
+            char character = configuredType.charAt(index);
+            if (character != '_' && character != '-' && 
!Character.isWhitespace(character)) {
+                normalized.append(character);
+            }
+        }
+        return normalized.toString().toUpperCase(Locale.ROOT);
+    }
+
+    private static long parseUnsigned32(String value, String configuredType) {
+        long parsed = Long.parseLong(value);
+        if (parsed < 0 || parsed > MAX_UNSIGNED_32) {
+            throw invalidRow(
+                    "SNMP sink value for type `"
+                            + configuredType
+                            + "` must be between 0 and "
+                            + MAX_UNSIGNED_32);
+        }
+        return parsed;
+    }
+
+    private static long parseTimeTicks(String value, String configuredType) {
+        if (DECIMAL_INTEGER.matcher(value).matches()) {
+            return parseUnsigned32(value, configuredType);
+        }
+
+        Matcher matcher = FORMATTED_TIME_TICKS.matcher(value);
+        if (!matcher.matches()) {
+            throw invalidRow(
+                    "SNMP sink TimeTicks value must be an unsigned decimal 
count or use the "
+                            + "SNMP4J format `[days, ]hours:mm:ss.hh`");
+        }
+
+        BigInteger days =
+                matcher.group(1) == null ? BigInteger.ZERO : new 
BigInteger(matcher.group(1));
+        String dayUnit = matcher.group(2);
+        if ((matcher.group(1) != null && BigInteger.ZERO.equals(days))
+                || (BigInteger.ONE.equals(days) && !"day".equals(dayUnit))
+                || (!BigInteger.ONE.equals(days) && "day".equals(dayUnit))) {
+            throw invalidRow("SNMP sink TimeTicks day unit does not match its 
value");
+        }
+
+        int hours = Integer.parseInt(matcher.group(3));
+        int minutes = Integer.parseInt(matcher.group(4));
+        int seconds = Integer.parseInt(matcher.group(5));
+        int hundredths = Integer.parseInt(matcher.group(6));
+        if (hours > 23 || minutes > 59 || seconds > 59) {
+            throw invalidRow("SNMP sink TimeTicks formatted value is outside 
clock bounds");
+        }
+
+        BigInteger ticks =
+                days.multiply(TICKS_PER_DAY)
+                        
.add(BigInteger.valueOf(hours).multiply(TICKS_PER_HOUR))
+                        
.add(BigInteger.valueOf(minutes).multiply(TICKS_PER_MINUTE))
+                        
.add(BigInteger.valueOf(seconds).multiply(TICKS_PER_SECOND))
+                        .add(BigInteger.valueOf(hundredths));
+        if (ticks.compareTo(BigInteger.valueOf(MAX_UNSIGNED_32)) > 0) {
+            throw invalidRow("SNMP sink TimeTicks value must be between 0 and 
" + MAX_UNSIGNED_32);
+        }
+        return ticks.longValue();
+    }
+
+    private static long parseUnsigned64(String value, String configuredType) {
+        BigInteger parsed = new BigInteger(value);
+        if (parsed.signum() < 0 || parsed.compareTo(MAX_UNSIGNED_64) > 0) {
+            throw invalidRow(
+                    "SNMP sink value for type `"
+                            + configuredType
+                            + "` must be between 0 and "
+                            + MAX_UNSIGNED_64);
+        }
+        return parsed.longValue();
+    }
+
+    private static byte[] parseHex(String value) {
+        if ((value.length() & 1) != 0 || !HEX_VALUE.matcher(value).matches()) {
+            throw invalidRow(
+                    "SNMP sink OctetStringHex value must contain an even 
number of hexadecimal characters");
+        }
+        byte[] bytes = new byte[value.length() / 2];
+        for (int index = 0; index < value.length(); index += 2) {
+            bytes[index / 2] = (byte) Integer.parseInt(value.substring(index, 
index + 2), 16);
+        }
+        return bytes;
+    }
+
+    private static void validateIpv4(String value) {
+        if (!IPV4_VALUE.matcher(value).matches()) {
+            throw invalidRow("SNMP sink IpAddress value must be a dotted IPv4 
address");
+        }
+        for (String octet : value.split("\\.")) {
+            if (Integer.parseInt(octet) > 255) {
+                throw invalidRow("SNMP sink IpAddress value must be a dotted 
IPv4 address");
+            }
+        }
+    }
+
+    private static SnmpConnectorException invalidConfig(String message) {
+        return new 
SnmpConnectorException(SnmpConnectorErrorCode.INVALID_CONFIG, message);
+    }
+
+    private static SnmpConnectorException invalidRow(String message) {
+        return new SnmpConnectorException(SnmpConnectorErrorCode.INVALID_ROW, 
message);
+    }
+
+    private static SnmpConnectorException invalidRow(String message, Throwable 
cause) {
+        return new SnmpConnectorException(SnmpConnectorErrorCode.INVALID_ROW, 
message, cause);
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkWriter.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkWriter.java
new file mode 100644
index 0000000000..a5d72ccdc0
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkWriter.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
+
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import 
org.apache.seatunnel.connectors.seatunnel.common.sink.AbstractSinkWriter;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkConfig;
+import 
org.apache.seatunnel.connectors.seatunnel.snmp.exception.SnmpConnectorErrorCode;
+import 
org.apache.seatunnel.connectors.seatunnel.snmp.exception.SnmpConnectorException;
+
+import java.io.IOException;
+
+/** Writes each row as one synchronous SNMPv2c SET request. */
+public final class SnmpSinkWriter extends AbstractSinkWriter<SeaTunnelRow, 
Void> {
+
+    private final SnmpSinkConfig config;
+    private final SnmpSinkRowConverter converter;
+    private final SnmpSetClient client;
+
+    public SnmpSinkWriter(SnmpSinkConfig config, SeaTunnelRowType rowType) {
+        this(config, rowType, Snmp4jSetClient::new);
+    }
+
+    SnmpSinkWriter(
+            SnmpSinkConfig config, SeaTunnelRowType rowType, 
SnmpSetClientFactory clientFactory) {
+        this.config = config;
+        this.converter = new SnmpSinkRowConverter(config, rowType);
+        try {
+            this.client = clientFactory.create(config);
+        } catch (IOException e) {
+            throw new SnmpConnectorException(
+                    SnmpConnectorErrorCode.CONNECTION_FAILED,
+                    "Failed to initialize SNMP SET client for agent "
+                            + config.getHost()
+                            + ":"
+                            + config.getPort(),
+                    e);
+        }
+    }
+
+    @Override
+    public void write(SeaTunnelRow row) {
+        SnmpSetRequest request = converter.convert(row);
+        try {
+            client.set(request);
+        } catch (IOException e) {
+            throw new SnmpConnectorException(
+                    SnmpConnectorErrorCode.WRITE_FAILED,
+                    "Failed to set OID "
+                            + request.getOid()
+                            + " on SNMP agent "
+                            + config.getHost()
+                            + ":"
+                            + config.getPort(),
+                    e);
+        }
+    }
+
+    @Override
+    public void close() {
+        try {
+            client.close();
+        } catch (IOException e) {
+            throw new SnmpConnectorException(
+                    SnmpConnectorErrorCode.CLOSE_FAILED,
+                    "Failed to close SNMP SET client for agent "
+                            + config.getHost()
+                            + ":"
+                            + config.getPort(),
+                    e);
+        }
+    }
+
+    interface SnmpSetClientFactory {
+        SnmpSetClient create(SnmpSinkConfig config) throws IOException;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/source/Snmp4jClient.java
 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/source/Snmp4jClient.java
index f4095e9fda..18430c46d7 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/source/Snmp4jClient.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/main/java/org/apache/seatunnel/connectors/seatunnel/snmp/source/Snmp4jClient.java
@@ -17,17 +17,14 @@
 
 package org.apache.seatunnel.connectors.seatunnel.snmp.source;
 
+import org.apache.seatunnel.connectors.seatunnel.snmp.client.SnmpTargetFactory;
 import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSourceConfig;
 
-import org.snmp4j.CommunityTarget;
 import org.snmp4j.PDU;
 import org.snmp4j.Snmp;
 import org.snmp4j.Target;
 import org.snmp4j.event.ResponseEvent;
-import org.snmp4j.mp.SnmpConstants;
 import org.snmp4j.smi.OID;
-import org.snmp4j.smi.OctetString;
-import org.snmp4j.smi.UdpAddress;
 import org.snmp4j.smi.Variable;
 import org.snmp4j.smi.VariableBinding;
 import org.snmp4j.transport.DefaultUdpTransportMapping;
@@ -102,13 +99,7 @@ final class Snmp4jClient implements SnmpClient {
     }
 
     static Target buildTarget(SnmpSourceConfig config) {
-        CommunityTarget target = new CommunityTarget();
-        target.setAddress(new UdpAddress(config.getHost() + "/" + 
config.getPort()));
-        target.setCommunity(new OctetString(config.getCommunity()));
-        target.setVersion(SnmpConstants.version2c);
-        target.setTimeout(config.getTimeoutMillis());
-        target.setRetries(config.getRetries());
-        return target;
+        return SnmpTargetFactory.create(config);
     }
 
     static List<SnmpRecord> extractRecords(PDU response) {
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/client/SnmpTargetFactoryTest.java
 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/client/SnmpTargetFactoryTest.java
new file mode 100644
index 0000000000..c360bc1acc
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/client/SnmpTargetFactoryTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.client;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkConfig;
+import 
org.apache.seatunnel.connectors.seatunnel.snmp.exception.SnmpConnectorException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class SnmpTargetFactoryTest {
+
+    @Test
+    void testInvalidAddressUsesConnectorExceptionWithoutCommunity() {
+        Map<String, Object> values = new HashMap<>();
+        values.put("host", "invalid/host");
+        values.put("community", "private-community");
+        SnmpSinkConfig config = new 
SnmpSinkConfig(ReadonlyConfig.fromMap(values));
+
+        SnmpConnectorException exception =
+                Assertions.assertThrows(
+                        SnmpConnectorException.class, () -> 
SnmpTargetFactory.create(config));
+
+        Assertions.assertTrue(exception.getMessage().contains("SNMP agent 
address"));
+        
Assertions.assertFalse(exception.getMessage().contains("private-community"));
+        Assertions.assertInstanceOf(IllegalArgumentException.class, 
exception.getCause());
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSinkConfigTest.java
 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSinkConfigTest.java
new file mode 100644
index 0000000000..86a8d30e24
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSinkConfigTest.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.config;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class SnmpSinkConfigTest {
+
+    @Test
+    void testDefaultsAndSharedTargetOptions() {
+        SnmpSinkConfig config = new 
SnmpSinkConfig(ReadonlyConfig.fromMap(baseConfig()));
+
+        Assertions.assertEquals("127.0.0.1", config.getHost());
+        Assertions.assertEquals(161, config.getPort());
+        Assertions.assertEquals("unit-test-community", config.getCommunity());
+        Assertions.assertEquals(5000L, config.getTimeoutMillis());
+        Assertions.assertEquals(1, config.getRetries());
+        Assertions.assertEquals("oid", config.getOidField());
+        Assertions.assertEquals("value", config.getValueField());
+        Assertions.assertEquals("value_type", config.getValueTypeField());
+    }
+
+    @Test
+    void testCustomFieldMappingIsTrimmed() {
+        Map<String, Object> values = baseConfig();
+        values.put("host", " 127.0.0.1 ");
+        values.put("community", " unit-test-community ");
+        values.put("oid_field", " target_oid ");
+        values.put("value_field", " target_value ");
+        values.put("value_type_field", " target_type ");
+
+        SnmpSinkConfig config = new 
SnmpSinkConfig(ReadonlyConfig.fromMap(values));
+
+        Assertions.assertEquals("127.0.0.1", config.getHost());
+        Assertions.assertEquals(" unit-test-community ", 
config.getCommunity());
+        Assertions.assertEquals("target_oid", config.getOidField());
+        Assertions.assertEquals("target_value", config.getValueField());
+        Assertions.assertEquals("target_type", config.getValueTypeField());
+    }
+
+    @Test
+    void testInvalidTargetOptionsDoNotDiscloseCommunity() {
+        Map<String, Object> invalidPort = baseConfig();
+        invalidPort.put("port", 65536);
+
+        IllegalArgumentException exception =
+                Assertions.assertThrows(
+                        IllegalArgumentException.class,
+                        () -> new 
SnmpSinkConfig(ReadonlyConfig.fromMap(invalidPort)));
+
+        
Assertions.assertFalse(exception.getMessage().contains("unit-test-community"));
+
+        Map<String, Object> invalidTimeout = baseConfig();
+        invalidTimeout.put("timeout_millis", 0L);
+        Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> new 
SnmpSinkConfig(ReadonlyConfig.fromMap(invalidTimeout)));
+
+        Map<String, Object> invalidRetries = baseConfig();
+        invalidRetries.put("retries", -1);
+        Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> new 
SnmpSinkConfig(ReadonlyConfig.fromMap(invalidRetries)));
+    }
+
+    @Test
+    void testMappedFieldsMustBeNonBlankAndDistinct() {
+        Map<String, Object> blank = baseConfig();
+        blank.put("oid_field", " ");
+        Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> new SnmpSinkConfig(ReadonlyConfig.fromMap(blank)));
+
+        Map<String, Object> duplicate = baseConfig();
+        duplicate.put("value_field", " oid ");
+        Assertions.assertThrows(
+                IllegalArgumentException.class,
+                () -> new SnmpSinkConfig(ReadonlyConfig.fromMap(duplicate)));
+    }
+
+    @Test
+    void testDirectConstructionRejectsBlankHostAndCommunity() {
+        for (String key : new String[] {"host", "community"}) {
+            Map<String, Object> values = baseConfig();
+            values.put(key, " \t\n");
+            Assertions.assertThrows(
+                    IllegalArgumentException.class,
+                    () -> new SnmpSinkConfig(ReadonlyConfig.fromMap(values)));
+        }
+    }
+
+    public static Map<String, Object> baseConfig() {
+        Map<String, Object> values = new HashMap<>();
+        values.put("host", "127.0.0.1");
+        values.put("community", "unit-test-community");
+        return values;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceConfigTest.java
 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceConfigTest.java
index 9435fc1028..7cd3aa9396 100644
--- 
a/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceConfigTest.java
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/config/SnmpSourceConfigTest.java
@@ -22,12 +22,26 @@ import 
org.apache.seatunnel.api.configuration.ReadonlyConfig;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.io.ByteArrayInputStream;
+import java.io.ObjectInputStream;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.HashMap;
 import java.util.Map;
 
 class SnmpSourceConfigTest {
 
+    private static final String LEGACY_SERIALIZED_CONFIG =
+            
"rO0ABXNyAEZvcmcuYXBhY2hlLnNlYXR1bm5lbC5jb25uZWN0b3JzLnNlYXR1bm5lbC5zbm1wLmNv"
+                    + 
"bmZpZy5Tbm1wU291cmNlQ29uZmlnAAAAAAAAAAECAAdKABJwb2xsSW50ZXJ2YWxNaWxsaXNJAARw"
+                    + 
"b3J0SQAHcmV0cmllc0oADXRpbWVvdXRNaWxsaXNMAAljb21tdW5pdHl0ABJMamF2YS9sYW5nL1N0"
+                    + 
"cmluZztMAARob3N0cQB+AAFMAARvaWRzdAAQTGphdmEvdXRpbC9MaXN0O3hwAAAAAAAA6mAAAACh"
+                    + 
"AAAAAQAAAAAAABOIdAAGcHVibGljdAAJMTI3LjAuMC4xc3IAI2phdmEudXRpbC5Db2xsZWN0aW9u"
+                    + 
"cyRTaW5nbGV0b25MaXN0Ku8pEDynm5cCAAFMAAdlbGVtZW50dAASTGphdmEvbGFuZy9PYmplY3Q7"
+                    + 
"eHBzcgASb3JnLnNubXA0ai5zbWkuT0lEaGJUgLBTOnQCAAFbAAV2YWx1ZXQAAltJeHIAH29yZy5z"
+                    + 
"bm1wNGouc21pLkFic3RyYWN0VmFyaWFibGUTXwXE8DKuiAIAAHhwdXIAAltJTbpgJnbqsqUCAAB4"
+                    + "cAAAAAQAAAABAAAAAwAAAAYAAAAB";
+
     @Test
     void testDefaultsAndOidNormalization() {
         SnmpSourceConfig config = new 
SnmpSourceConfig(ReadonlyConfig.fromMap(baseConfig()));
@@ -117,6 +131,26 @@ class SnmpSourceConfigTest {
                 () -> new SnmpSourceConfig(ReadonlyConfig.fromMap(interval)));
     }
 
+    @Test
+    void testDeserializesLegacySerializedLayout() throws Exception {
+        byte[] serialized = 
Base64.getDecoder().decode(LEGACY_SERIALIZED_CONFIG);
+
+        SnmpSourceConfig config;
+        try (ObjectInputStream input =
+                new ObjectInputStream(new ByteArrayInputStream(serialized))) {
+            config = (SnmpSourceConfig) input.readObject();
+        }
+
+        Assertions.assertEquals("127.0.0.1", config.getHost());
+        Assertions.assertEquals(161, config.getPort());
+        Assertions.assertEquals("public", config.getCommunity());
+        Assertions.assertEquals(5000L, config.getTimeoutMillis());
+        Assertions.assertEquals(1, config.getRetries());
+        Assertions.assertEquals(60000L, config.getPollIntervalMillis());
+        Assertions.assertEquals(1, config.getOids().size());
+        Assertions.assertEquals("1.3.6.1", config.getOids().get(0).toString());
+    }
+
     static Map<String, Object> baseConfig() {
         Map<String, Object> values = new HashMap<>();
         values.put("host", "127.0.0.1");
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/Snmp4jSetClientTest.java
 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/Snmp4jSetClientTest.java
new file mode 100644
index 0000000000..7f516bdeb9
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/Snmp4jSetClientTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkConfig;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.snmp4j.CommandResponderEvent;
+import org.snmp4j.MessageException;
+import org.snmp4j.PDU;
+import org.snmp4j.Snmp;
+import org.snmp4j.mp.StatusInformation;
+import org.snmp4j.smi.Integer32;
+import org.snmp4j.smi.OID;
+import org.snmp4j.smi.UdpAddress;
+import org.snmp4j.smi.VariableBinding;
+import org.snmp4j.transport.DefaultUdpTransportMapping;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+class Snmp4jSetClientTest {
+
+    @Test
+    void testBuildsSingleBindingSetRequest() {
+        SnmpSetRequest request =
+                new SnmpSetRequest(new OID("1.3.6.1.2.1.1.5.0"), new 
Integer32(42));
+
+        PDU pdu = Snmp4jSetClient.buildSetRequest(request);
+
+        Assertions.assertEquals(PDU.SET, pdu.getType());
+        Assertions.assertEquals(1, pdu.size());
+        Assertions.assertEquals("1.3.6.1.2.1.1.5.0", 
pdu.get(0).getOid().toString());
+        Assertions.assertEquals("42", pdu.get(0).getVariable().toString());
+    }
+
+    @Test
+    void testSendsSetToLoopbackSnmpAgent() throws Exception {
+        AtomicReference<VariableBinding> received = new AtomicReference<>();
+        try (LoopbackAgent agent = new LoopbackAgent(PDU.noError, received);
+                Snmp4jSetClient client = new 
Snmp4jSetClient(config(agent.getPort()))) {
+            client.set(
+                    new SnmpSetRequest(
+                            new OID("1.3.6.1.2.1.1.5.0"),
+                            SnmpSinkRowConverter.parseVariable("OctetString", 
"router-1")));
+        }
+
+        Assertions.assertNotNull(received.get());
+        Assertions.assertEquals("1.3.6.1.2.1.1.5.0", 
received.get().getOid().toString());
+        Assertions.assertEquals("router-1", 
received.get().getVariable().toString());
+    }
+
+    @Test
+    void testRemoteErrorStatusIsReported() throws Exception {
+        AtomicReference<VariableBinding> received = new AtomicReference<>();
+        try (LoopbackAgent agent = new LoopbackAgent(PDU.notWritable, 
received);
+                Snmp4jSetClient client = new 
Snmp4jSetClient(config(agent.getPort()))) {
+            IOException exception =
+                    Assertions.assertThrows(
+                            IOException.class,
+                            () ->
+                                    client.set(
+                                            new SnmpSetRequest(
+                                                    new 
OID("1.3.6.1.2.1.1.5.0"),
+                                                    new Integer32(42))));
+
+            Assertions.assertTrue(
+                    exception.getMessage().contains("error status " + 
PDU.notWritable));
+            Assertions.assertTrue(exception.getMessage().contains("index 1"));
+            
Assertions.assertFalse(exception.getMessage().contains("unit-test-community"));
+        }
+    }
+
+    private static SnmpSinkConfig config(int port) {
+        Map<String, Object> values = new HashMap<>();
+        values.put("host", "127.0.0.1");
+        values.put("community", "unit-test-community");
+        values.put("port", port);
+        values.put("timeout_millis", 1000L);
+        values.put("retries", 0);
+        return new SnmpSinkConfig(ReadonlyConfig.fromMap(values));
+    }
+
+    private static final class LoopbackAgent implements AutoCloseable {
+        private final int errorStatus;
+        private final AtomicReference<VariableBinding> received;
+        private final DefaultUdpTransportMapping transport;
+        private final Snmp agent;
+        private final AtomicReference<MessageException> responseFailure = new 
AtomicReference<>();
+
+        private LoopbackAgent(int errorStatus, 
AtomicReference<VariableBinding> received)
+                throws IOException {
+            this.errorStatus = errorStatus;
+            this.received = received;
+            this.transport =
+                    new DefaultUdpTransportMapping(
+                            new UdpAddress(InetAddress.getLoopbackAddress(), 
0));
+            this.agent = new Snmp(transport);
+            agent.addCommandResponder(this::respond);
+            agent.listen();
+        }
+
+        private int getPort() {
+            return transport.getListenAddress().getPort();
+        }
+
+        private void respond(CommandResponderEvent event) {
+            PDU request = event.getPDU();
+            received.set(request.get(0));
+            PDU response = new PDU(request);
+            response.setType(PDU.RESPONSE);
+            response.setErrorStatus(errorStatus);
+            response.setErrorIndex(errorStatus == PDU.noError ? 0 : 1);
+            try {
+                event.getMessageDispatcher()
+                        .returnResponsePdu(
+                                event.getMessageProcessingModel(),
+                                event.getSecurityModel(),
+                                event.getSecurityName(),
+                                event.getSecurityLevel(),
+                                response,
+                                event.getMaxSizeResponsePDU(),
+                                event.getStateReference(),
+                                new StatusInformation());
+                event.setProcessed(true);
+            } catch (MessageException e) {
+                responseFailure.set(e);
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            agent.close();
+            if (responseFailure.get() != null) {
+                throw new IOException(
+                        "Loopback SNMP agent failed to send response", 
responseFailure.get());
+            }
+        }
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkFactoryTest.java
 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkFactoryTest.java
new file mode 100644
index 0000000000..215678cc8a
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkFactoryTest.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
+
+import org.apache.seatunnel.api.configuration.Option;
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConfigValidator;
+import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.PhysicalColumn;
+import org.apache.seatunnel.api.table.catalog.TableIdentifier;
+import org.apache.seatunnel.api.table.catalog.TableSchema;
+import org.apache.seatunnel.api.table.factory.TableSinkFactoryContext;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.common.utils.SerializationUtils;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkOptions;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+class SnmpSinkFactoryTest {
+
+    @Test
+    void testOptionRuleAcceptsDefaultsAndNumericBoundaries() {
+        validate(baseConfig());
+        Map<String, Object> values = baseConfig();
+        values.put("port", 1);
+        values.put("timeout_millis", 1L);
+        values.put("retries", 0);
+        validate(values);
+        values.put("port", 65535);
+        values.put("timeout_millis", Long.MAX_VALUE);
+        values.put("retries", Integer.MAX_VALUE);
+        validate(values);
+    }
+
+    @Test
+    void testOptionRuleRejectsMissingRequiredOptions() {
+        for (String key : new String[] {"host", "community"}) {
+            Map<String, Object> values = baseConfig();
+            values.remove(key);
+            Assertions.assertThrows(OptionValidationException.class, () -> 
validate(values), key);
+        }
+    }
+
+    @Test
+    void testOptionRuleRejectsBlankStrings() {
+        for (String key :
+                new String[] {
+                    "host", "community", "oid_field", "value_field", 
"value_type_field"
+                }) {
+            assertInvalidOption(key, "");
+            assertInvalidOption(key, " \t\n");
+        }
+    }
+
+    @Test
+    void testOptionRuleRejectsInvalidNumericValuesWithoutDisclosingCommunity() 
{
+        assertInvalidOption("port", 0);
+        assertInvalidOption("port", 65536);
+        assertInvalidOption("timeout_millis", 0L);
+        assertInvalidOption("timeout_millis", -1L);
+        assertInvalidOption("retries", -1);
+    }
+
+    @Test
+    void testOptionRulePreservesWhitespaceForRuntimeNormalization() {
+        Map<String, Object> values = baseConfig();
+        values.put("host", " 127.0.0.1 ");
+        values.put("community", " unit-test-community ");
+        values.put("oid_field", " oid ");
+        values.put("value_field", " value ");
+        values.put("value_type_field", " value_type ");
+        ReadonlyConfig config = ReadonlyConfig.fromMap(values);
+        ConfigValidator.of(config).validate(new 
SnmpSinkFactory().optionRule());
+        Assertions.assertEquals(" unit-test-community ", 
config.get(SnmpSinkOptions.COMMUNITY));
+    }
+
+    private static void assertInvalidOption(String key, Object value) {
+        Map<String, Object> values = baseConfig();
+        values.put(key, value);
+        OptionValidationException exception =
+                Assertions.assertThrows(OptionValidationException.class, () -> 
validate(values));
+        Assertions.assertTrue(exception.getMessage().contains(key));
+        
Assertions.assertFalse(exception.getMessage().contains("unit-test-community"));
+    }
+
+    private static void validate(Map<String, Object> values) {
+        ConfigValidator.of(ReadonlyConfig.fromMap(values))
+                .validate(new SnmpSinkFactory().optionRule());
+    }
+
+    @Test
+    void testFactoryIdentityAndOptions() {
+        SnmpSinkFactory factory = new SnmpSinkFactory();
+
+        Assertions.assertEquals(SnmpSinkOptions.CONNECTOR_IDENTITY, 
factory.factoryIdentifier());
+        OptionRule rule = factory.optionRule();
+        List<Option<?>> required =
+                rule.getRequiredOptions().stream()
+                        .flatMap(group -> group.getOptions().stream())
+                        .collect(Collectors.toList());
+        Assertions.assertTrue(required.contains(SnmpSinkOptions.HOST));
+        Assertions.assertTrue(required.contains(SnmpSinkOptions.COMMUNITY));
+        
Assertions.assertTrue(rule.getOptionalOptions().contains(SnmpSinkOptions.PORT));
+        
Assertions.assertTrue(rule.getOptionalOptions().contains(SnmpSinkOptions.TIMEOUT_MILLIS));
+        
Assertions.assertTrue(rule.getOptionalOptions().contains(SnmpSinkOptions.RETRIES));
+        
Assertions.assertTrue(rule.getOptionalOptions().contains(SnmpSinkOptions.OID_FIELD));
+        
Assertions.assertTrue(rule.getOptionalOptions().contains(SnmpSinkOptions.VALUE_FIELD));
+        
Assertions.assertTrue(rule.getOptionalOptions().contains(SnmpSinkOptions.VALUE_TYPE_FIELD));
+    }
+
+    @Test
+    void testFactoryCreatesSerializableSinkWithSourceCompatibleSchema() {
+        CatalogTable catalogTable = catalogTable();
+        TableSinkFactoryContext context =
+                new TableSinkFactoryContext(
+                        catalogTable,
+                        ReadonlyConfig.fromMap(baseConfig()),
+                        getClass().getClassLoader());
+
+        SnmpSink sink = (SnmpSink) new 
SnmpSinkFactory().createSink(context).createSink();
+        SnmpSink restored = 
SerializationUtils.deserialize(SerializationUtils.serialize(sink));
+
+        Assertions.assertEquals("SNMP", restored.getPluginName());
+        Assertions.assertArrayEquals(
+                catalogTable.getSeaTunnelRowType().getFieldNames(),
+                restored.getWriteCatalogTable()
+                        .orElseThrow(() -> new AssertionError("Sink catalog 
table is missing"))
+                        .getSeaTunnelRowType()
+                        .getFieldNames());
+    }
+
+    private static CatalogTable catalogTable() {
+        TableSchema schema =
+                TableSchema.builder()
+                        .column(
+                                PhysicalColumn.of(
+                                        "agent", BasicType.STRING_TYPE, 0, 
false, null, null))
+                        .column(
+                                PhysicalColumn.of(
+                                        "oid", BasicType.STRING_TYPE, 0, 
false, null, null))
+                        .column(
+                                PhysicalColumn.of(
+                                        "value", BasicType.STRING_TYPE, 0, 
false, null, null))
+                        .column(
+                                PhysicalColumn.of(
+                                        "value_type", BasicType.STRING_TYPE, 
0, false, null, null))
+                        .column(
+                                PhysicalColumn.of(
+                                        "poll_time", BasicType.LONG_TYPE, 0, 
false, null, null))
+                        .build();
+        return CatalogTable.of(
+                TableIdentifier.of("default", "default", "snmp_sink_test"),
+                schema,
+                Collections.emptyMap(),
+                Collections.emptyList(),
+                "SNMP sink test table");
+    }
+
+    private static Map<String, Object> baseConfig() {
+        Map<String, Object> values = new HashMap<>();
+        values.put("host", "127.0.0.1");
+        values.put("community", "unit-test-community");
+        return values;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkRowConverterTest.java
 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkRowConverterTest.java
new file mode 100644
index 0000000000..60bc34ed01
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkRowConverterTest.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkConfig;
+import 
org.apache.seatunnel.connectors.seatunnel.snmp.exception.SnmpConnectorException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.snmp4j.smi.Counter32;
+import org.snmp4j.smi.Counter64;
+import org.snmp4j.smi.Gauge32;
+import org.snmp4j.smi.Integer32;
+import org.snmp4j.smi.IpAddress;
+import org.snmp4j.smi.OID;
+import org.snmp4j.smi.OctetString;
+import org.snmp4j.smi.TimeTicks;
+import org.snmp4j.smi.UnsignedInteger32;
+import org.snmp4j.smi.Variable;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+class SnmpSinkRowConverterTest {
+
+    @Test
+    void testConvertsSourceCompatibleSchema() {
+        SnmpSinkRowConverter converter = new SnmpSinkRowConverter(config(), 
sourceRowType());
+
+        SnmpSetRequest request =
+                converter.convert(
+                        new SeaTunnelRow(
+                                new Object[] {
+                                    "127.0.0.1:161",
+                                    ".1.3.6.1.2.1.1.5.0",
+                                    "router-1",
+                                    "OctetString",
+                                    1234L
+                                }));
+
+        Assertions.assertEquals("1.3.6.1.2.1.1.5.0", 
request.getOid().toString());
+        Assertions.assertEquals("router-1", request.getValue().toString());
+        Assertions.assertInstanceOf(OctetString.class, request.getValue());
+    }
+
+    @Test
+    void testSupportsDocumentedSmiTypes() {
+        assertVariable("Integer32", "-42", Integer32.class, "-42");
+        assertVariable("UnsignedInteger32", "4294967295", 
UnsignedInteger32.class, "4294967295");
+        assertVariable("Counter32", "12", Counter32.class, "12");
+        assertVariable("Gauge32", "13", Gauge32.class, "13");
+        assertVariable("TimeTicks", "14", TimeTicks.class, "0:00:00.14");
+        assertVariable(
+                "Counter64", "18446744073709551615", Counter64.class, 
"18446744073709551615");
+        assertVariable("OctetString", "router-1", OctetString.class, 
"router-1");
+
+        Variable hex = SnmpSinkRowConverter.parseVariable("OctetStringHex", 
"00ff10");
+        Assertions.assertInstanceOf(OctetString.class, hex);
+        Assertions.assertArrayEquals(
+                new byte[] {0x00, (byte) 0xff, 0x10}, ((OctetString) 
hex).getValue());
+
+        assertVariable("OID", "1.3.6.1.2.1", OID.class, "1.3.6.1.2.1");
+        assertVariable("IpAddress", "192.0.2.10", IpAddress.class, 
"192.0.2.10");
+    }
+
+    @Test
+    void testSupportsSnmp4jSourceSyntaxStringsAndValues() {
+        assertSourceVariable(new Integer32(-42), Integer32.class);
+        assertSourceVariable(new UnsignedInteger32(11), Gauge32.class);
+        assertSourceVariable(new Counter32(12), Counter32.class);
+        assertSourceVariable(new Gauge32(13), Gauge32.class);
+        assertSourceVariable(new TimeTicks(14), TimeTicks.class);
+        assertSourceVariable(new TimeTicks(172_800_014L), TimeTicks.class);
+        assertSourceVariable(new Counter64(15), Counter64.class);
+        assertSourceVariable(new OctetString("router-1"), OctetString.class);
+        assertSourceVariable(new OID("1.3.6.1.2.1"), OID.class);
+        assertSourceVariable(new IpAddress("192.0.2.10"), IpAddress.class);
+    }
+
+    @Test
+    void testPreservesOctetStringPayload() {
+        SnmpSinkRowConverter converter = new SnmpSinkRowConverter(config(), 
sinkRowType());
+
+        SnmpSetRequest whitespace =
+                converter.convert(row("1.3.6.1.2.1.1.5.0", "  router-1  ", " 
OctetString "));
+        Assertions.assertEquals("  router-1  ", 
whitespace.getValue().toString());
+
+        SnmpSetRequest empty = converter.convert(row("1.3.6.1.2.1.1.5.0", "", 
"OctetString"));
+        Assertions.assertEquals(0, ((OctetString) empty.getValue()).length());
+
+        SnmpSetRequest utf8 =
+                converter.convert(row("1.3.6.1.2.1.1.5.0", "router-\u03b1", 
"OctetString"));
+        Assertions.assertArrayEquals(
+                "router-\u03b1".getBytes(StandardCharsets.UTF_8),
+                ((OctetString) utf8.getValue()).getValue());
+    }
+
+    @Test
+    void testRejectsInvalidSchemaBeforeClientCreation() {
+        SeaTunnelRowType missingField =
+                new SeaTunnelRowType(
+                        new String[] {"oid", "value"},
+                        new SeaTunnelDataType[] {BasicType.STRING_TYPE, 
BasicType.STRING_TYPE});
+        SnmpConnectorException missing =
+                Assertions.assertThrows(
+                        SnmpConnectorException.class,
+                        () -> new SnmpSinkRowConverter(config(), 
missingField));
+        Assertions.assertTrue(missing.getMessage().contains("value_type"));
+
+        SeaTunnelRowType wrongType =
+                new SeaTunnelRowType(
+                        new String[] {"oid", "value", "value_type"},
+                        new SeaTunnelDataType[] {
+                            BasicType.STRING_TYPE, BasicType.INT_TYPE, 
BasicType.STRING_TYPE
+                        });
+        SnmpConnectorException invalidType =
+                Assertions.assertThrows(
+                        SnmpConnectorException.class,
+                        () -> new SnmpSinkRowConverter(config(), wrongType));
+        Assertions.assertTrue(invalidType.getMessage().contains("must use 
STRING"));
+    }
+
+    @Test
+    void testRejectsInvalidRowsBeforeNetworkIo() {
+        SnmpSinkRowConverter converter = new SnmpSinkRowConverter(config(), 
sinkRowType());
+
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () -> converter.convert(row("not-an-oid", "1", "Integer32")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () -> converter.convert(row("1.3.6.1.2.1.1.5.0", "1", 
"UnknownType")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () -> converter.convert(row("1.3.6.1.2.1.1.5.0", "4294967296", 
"Counter32")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () -> converter.convert(row("1.3.6.1.2.1.1.5.0", "0fg1", 
"OctetStringHex")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () -> converter.convert(row("1.3.6.1.2.1.1.5.0", "999.0.2.1", 
"IpAddress")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () -> converter.convert(row("1.3.6.1.2.1.1.5.0", null, 
"OctetString")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () ->
+                        converter.convert(
+                                new SeaTunnelRow(
+                                        new Object[] {"1.3.6.1.2.1.1.5.0", 1, 
"Integer32"})));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () ->
+                        converter.convert(
+                                row("1.3.6.1.2.1.1.5.0", 
"18446744073709551616", "Counter64")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () -> converter.convert(row("1.3.6.1.2.1.1.5.0", "0:60:00.00", 
"TimeTicks")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () ->
+                        converter.convert(
+                                row("1.3.6.1.2.1.1.5.0", "2 day, 0:00:00.00", 
"TimeTicks")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () ->
+                        converter.convert(
+                                row("1.3.6.1.2.1.1.5.0", "0 days, 0:00:00.00", 
"TimeTicks")));
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () ->
+                        converter.convert(
+                                row("1.3.6.1.2.1.1.5.0", "498 days, 
0:00:00.00", "TimeTicks")));
+    }
+
+    private static void assertVariable(
+            String type,
+            String value,
+            Class<? extends Variable> expectedClass,
+            String expectedText) {
+        Variable variable = SnmpSinkRowConverter.parseVariable(type, value);
+        Assertions.assertInstanceOf(expectedClass, variable);
+        Assertions.assertEquals(expectedText, variable.toString());
+    }
+
+    private static void assertSourceVariable(
+            Variable sourceVariable, Class<? extends Variable> expectedClass) {
+        Variable converted =
+                SnmpSinkRowConverter.parseVariable(
+                        sourceVariable.getSyntaxString(), 
sourceVariable.toString());
+        Assertions.assertInstanceOf(expectedClass, converted);
+        Assertions.assertEquals(sourceVariable.toString(), 
converted.toString());
+    }
+
+    private static SeaTunnelRow row(String oid, String value, String 
valueType) {
+        return new SeaTunnelRow(new Object[] {oid, value, valueType});
+    }
+
+    static SeaTunnelRowType sinkRowType() {
+        return new SeaTunnelRowType(
+                new String[] {"oid", "value", "value_type"},
+                new SeaTunnelDataType[] {
+                    BasicType.STRING_TYPE, BasicType.STRING_TYPE, 
BasicType.STRING_TYPE
+                });
+    }
+
+    private static SeaTunnelRowType sourceRowType() {
+        return new SeaTunnelRowType(
+                new String[] {"agent", "oid", "value", "value_type", 
"poll_time"},
+                new SeaTunnelDataType[] {
+                    BasicType.STRING_TYPE,
+                    BasicType.STRING_TYPE,
+                    BasicType.STRING_TYPE,
+                    BasicType.STRING_TYPE,
+                    BasicType.LONG_TYPE
+                });
+    }
+
+    static SnmpSinkConfig config() {
+        Map<String, Object> values = new HashMap<>();
+        values.put("host", "127.0.0.1");
+        values.put("community", "unit-test-community");
+        return new SnmpSinkConfig(ReadonlyConfig.fromMap(values));
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkWriterTest.java
 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkWriterTest.java
new file mode 100644
index 0000000000..a268a01931
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-snmp/src/test/java/org/apache/seatunnel/connectors/seatunnel/snmp/sink/SnmpSinkWriterTest.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.snmp.sink;
+
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.connectors.seatunnel.snmp.config.SnmpSinkConfig;
+import 
org.apache.seatunnel.connectors.seatunnel.snmp.exception.SnmpConnectorException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+class SnmpSinkWriterTest {
+
+    @Test
+    void testWritesOneSynchronousSetPerRowAndClosesClient() {
+        FakeSnmpSetClient client = new FakeSnmpSetClient();
+        SnmpSinkWriter writer = writer(client);
+
+        writer.write(row());
+        writer.close();
+
+        Assertions.assertEquals(1, client.writeCount);
+        Assertions.assertEquals("1.3.6.1.2.1.1.5.0", 
client.request.getOid().toString());
+        Assertions.assertEquals("router-1", 
client.request.getValue().toString());
+        Assertions.assertTrue(client.closed);
+    }
+
+    @Test
+    void testInvalidRowDoesNotReachClient() {
+        FakeSnmpSetClient client = new FakeSnmpSetClient();
+        SnmpSinkWriter writer = writer(client);
+
+        Assertions.assertThrows(
+                SnmpConnectorException.class,
+                () ->
+                        writer.write(
+                                new SeaTunnelRow(
+                                        new Object[] {"invalid", "router-1", 
"OctetString"})));
+
+        Assertions.assertEquals(0, client.writeCount);
+    }
+
+    @Test
+    void testWriteFailureUsesConnectorErrorWithoutCommunity() {
+        FakeSnmpSetClient client = new FakeSnmpSetClient();
+        client.writeFailure = new IOException("remote timeout");
+        SnmpSinkWriter writer = writer(client);
+
+        SnmpConnectorException exception =
+                Assertions.assertThrows(SnmpConnectorException.class, () -> 
writer.write(row()));
+
+        Assertions.assertTrue(exception.getMessage().contains("SNMP-03"));
+        
Assertions.assertTrue(exception.getMessage().contains("1.3.6.1.2.1.1.5.0"));
+        
Assertions.assertFalse(exception.getMessage().contains("unit-test-community"));
+    }
+
+    @Test
+    void testConnectionAndCloseFailuresUseConnectorErrorsWithoutCommunity() {
+        SnmpSinkConfig config = SnmpSinkRowConverterTest.config();
+        SnmpConnectorException connectionFailure =
+                Assertions.assertThrows(
+                        SnmpConnectorException.class,
+                        () ->
+                                new SnmpSinkWriter(
+                                        config,
+                                        SnmpSinkRowConverterTest.sinkRowType(),
+                                        ignored -> {
+                                            throw new IOException("bind 
failed");
+                                        }));
+        
Assertions.assertTrue(connectionFailure.getMessage().contains("SNMP-01"));
+        
Assertions.assertFalse(connectionFailure.getMessage().contains("unit-test-community"));
+
+        FakeSnmpSetClient client = new FakeSnmpSetClient();
+        client.closeFailure = new IOException("close failed");
+        SnmpConnectorException closeFailure =
+                Assertions.assertThrows(SnmpConnectorException.class, () -> 
writer(client).close());
+        Assertions.assertTrue(closeFailure.getMessage().contains("SNMP-06"));
+        
Assertions.assertFalse(closeFailure.getMessage().contains("unit-test-community"));
+    }
+
+    private static SnmpSinkWriter writer(FakeSnmpSetClient client) {
+        return new SnmpSinkWriter(
+                SnmpSinkRowConverterTest.config(),
+                SnmpSinkRowConverterTest.sinkRowType(),
+                ignored -> client);
+    }
+
+    private static SeaTunnelRow row() {
+        return new SeaTunnelRow(new Object[] {"1.3.6.1.2.1.1.5.0", "router-1", 
"OctetString"});
+    }
+
+    private static final class FakeSnmpSetClient implements SnmpSetClient {
+        private int writeCount;
+        private boolean closed;
+        private SnmpSetRequest request;
+        private IOException writeFailure;
+        private IOException closeFailure;
+
+        @Override
+        public void set(SnmpSetRequest request) throws IOException {
+            writeCount++;
+            this.request = request;
+            if (writeFailure != null) {
+                throw writeFailure;
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            closed = true;
+            if (closeFailure != null) {
+                throw closeFailure;
+            }
+        }
+    }
+}
diff --git 
a/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigShadeUtils.java
 
b/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigShadeUtils.java
index 52fa94c6a1..706ffc8237 100644
--- 
a/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigShadeUtils.java
+++ 
b/seatunnel-core/seatunnel-core-starter/src/main/java/org/apache/seatunnel/core/starter/utils/ConfigShadeUtils.java
@@ -55,7 +55,7 @@ public final class ConfigShadeUtils {
             new String[] {"password", "username", "auth", "token", 
"access_key", "secret_key"};
 
     private static final String[] DEFAULT_LOG_MASK_ONLY_KEYWORDS =
-            new String[] {"sasl.jaas.config"};
+            new String[] {"sasl.jaas.config", "community"};
 
     private static final Map<String, ConfigShade> CONFIG_SHADES = new 
HashMap<>();
 
diff --git 
a/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigBuilderTest.java
 
b/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigBuilderTest.java
index 43f711de3c..fe1b71f7a9 100644
--- 
a/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigBuilderTest.java
+++ 
b/seatunnel-core/seatunnel-core-starter/src/test/java/org/apache/seatunnel/core/starter/utils/ConfigBuilderTest.java
@@ -101,6 +101,25 @@ public class ConfigBuilderTest {
         Assertions.assertEquals("localhost:9092", 
desensitizedKafkaConfig.get("bootstrap.servers"));
     }
 
+    @Test
+    public void testConfigDesensitizationMasksSnmpCommunity() {
+        Map<String, Object> sink = new LinkedHashMap<>();
+        sink.put("host", "127.0.0.1");
+        sink.put("community", "private-community");
+
+        Map<String, Object> config = new LinkedHashMap<>();
+        config.put("sink", Arrays.asList(sink));
+
+        Map<String, Object> desensitized =
+                ConfigBuilder.configDesensitization(
+                        config, 
ConfigShadeUtils.getLogDesensitizationOptions(null));
+        List<?> sinks = (List<?>) desensitized.get("sink");
+        Map<?, ?> desensitizedSink = (Map<?, ?>) sinks.get(0);
+
+        Assertions.assertEquals("******", desensitizedSink.get("community"));
+        Assertions.assertEquals("127.0.0.1", desensitizedSink.get("host"));
+    }
+
     @Test
     public void testConfigDesensitizationMasksS3CredentialOptions() {
         Map<String, Object> accessKeyConfig = new LinkedHashMap<>();
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/pom.xml 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/pom.xml
new file mode 100644
index 0000000000..b5321d9de2
--- /dev/null
+++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+       http://www.apache.org/licenses/LICENSE-2.0
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.seatunnel</groupId>
+        <artifactId>seatunnel-connector-v2-e2e</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <artifactId>connector-snmp-e2e</artifactId>
+    <name>SeaTunnel : E2E : Connector V2 : SNMP</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-fake</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-snmp</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/snmp/SnmpAgent.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/snmp/SnmpAgent.java
new file mode 100644
index 0000000000..325e904035
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/snmp/SnmpAgent.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seatunnel.e2e.connector.snmp;
+
+import org.snmp4j.CommandResponderEvent;
+import org.snmp4j.PDU;
+import org.snmp4j.Snmp;
+import org.snmp4j.mp.StatusInformation;
+import org.snmp4j.smi.UdpAddress;
+import org.snmp4j.smi.VariableBinding;
+import org.snmp4j.transport.DefaultUdpTransportMapping;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.concurrent.CountDownLatch;
+
+/** Minimal SNMP responder used to verify the sink over a real UDP transport. 
*/
+public final class SnmpAgent {
+
+    private static final int PORT = 1161;
+    private static final Path OUTPUT = Paths.get("/tmp/snmp-set.txt");
+
+    private SnmpAgent() {}
+
+    public static void main(String[] args) throws Exception {
+        DefaultUdpTransportMapping transport =
+                new DefaultUdpTransportMapping(new UdpAddress("0.0.0.0/" + 
PORT));
+        Snmp agent = new Snmp(transport);
+        agent.addCommandResponder(SnmpAgent::respond);
+        Runtime.getRuntime().addShutdownHook(new Thread(() -> close(agent)));
+        agent.listen();
+        System.out.println("snmp-agent-ready");
+        new CountDownLatch(1).await();
+    }
+
+    private static void respond(CommandResponderEvent event) {
+        PDU request = event.getPDU();
+        if (request == null || request.getType() != PDU.SET || request.size() 
!= 1) {
+            return;
+        }
+
+        VariableBinding binding = request.get(0);
+        try {
+            Files.write(
+                    OUTPUT,
+                    (binding.getOid() + "=" + binding.getVariable() + 
System.lineSeparator())
+                            .getBytes(StandardCharsets.UTF_8),
+                    StandardOpenOption.CREATE,
+                    StandardOpenOption.TRUNCATE_EXISTING);
+            PDU response = new PDU(request);
+            response.setType(PDU.RESPONSE);
+            response.setErrorStatus(PDU.noError);
+            response.setErrorIndex(0);
+            event.getMessageDispatcher()
+                    .returnResponsePdu(
+                            event.getMessageProcessingModel(),
+                            event.getSecurityModel(),
+                            event.getSecurityName(),
+                            event.getSecurityLevel(),
+                            response,
+                            event.getMaxSizeResponsePDU(),
+                            event.getStateReference(),
+                            new StatusInformation());
+            event.setProcessed(true);
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to handle SNMP SET 
request", e);
+        }
+    }
+
+    private static void close(Snmp agent) {
+        try {
+            agent.close();
+        } catch (IOException e) {
+            System.err.println("Failed to close SNMP agent: " + 
e.getMessage());
+        }
+    }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/snmp/SnmpSinkIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/snmp/SnmpSinkIT.java
new file mode 100644
index 0000000000..0198713c15
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/snmp/SnmpSinkIT.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seatunnel.e2e.connector.snmp;
+
+import org.apache.seatunnel.e2e.common.TestResource;
+import org.apache.seatunnel.e2e.common.TestSuiteBase;
+import org.apache.seatunnel.e2e.common.container.EngineType;
+import org.apache.seatunnel.e2e.common.container.TestContainer;
+import org.apache.seatunnel.e2e.common.junit.DisabledOnContainer;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestTemplate;
+import org.snmp4j.Snmp;
+import org.testcontainers.containers.Container;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.lifecycle.Startables;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.stream.Stream;
+
+@DisabledOnContainer(
+        value = {},
+        type = {EngineType.SPARK, EngineType.FLINK},
+        disabledReason = "The first SNMP sink E2E slice targets the Zeta 
engine")
+public class SnmpSinkIT extends TestSuiteBase implements TestResource {
+
+    private static final DockerImageName JAVA_IMAGE =
+            DockerImageName.parse("eclipse-temurin:11-jre-jammy");
+    private static final String EXPECTED_SET = 
"1.3.6.1.2.1.1.4.0=seatunnel-e2e";
+
+    private GenericContainer<?> snmpAgent;
+
+    @Override
+    @BeforeAll
+    public void startUp() throws URISyntaxException {
+        Path testClasses =
+                Paths.get(
+                        SnmpAgent.class
+                                .getProtectionDomain()
+                                .getCodeSource()
+                                .getLocation()
+                                .toURI());
+        Path snmp4jJar =
+                
Paths.get(Snmp.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+
+        snmpAgent =
+                new GenericContainer<>(JAVA_IMAGE)
+                        .withNetwork(NETWORK)
+                        .withNetworkAliases("snmp-agent")
+                        .withCopyFileToContainer(
+                                MountableFile.forHostPath(testClasses), 
"/opt/snmp-agent/classes")
+                        .withCopyFileToContainer(
+                                MountableFile.forHostPath(snmp4jJar), 
"/opt/snmp-agent/snmp4j.jar")
+                        .withCommand(
+                                "java",
+                                "-cp",
+                                
"/opt/snmp-agent/classes:/opt/snmp-agent/snmp4j.jar",
+                                SnmpAgent.class.getName())
+                        .waitingFor(
+                                Wait.forLogMessage(".*snmp-agent-ready.*\\n", 
1)
+                                        
.withStartupTimeout(Duration.ofMinutes(1)));
+        Startables.deepStart(Stream.of(snmpAgent)).join();
+    }
+
+    @Override
+    public void tearDown() {
+        if (snmpAgent != null) {
+            snmpAgent.close();
+        }
+    }
+
+    @TestTemplate
+    public void testFakeSourceWritesSnmpSet(TestContainer container) throws 
Exception {
+        Container.ExecResult execResult = 
container.executeJob("/fake_to_snmp.conf");
+        Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
+
+        Awaitility.await()
+                .atMost(Duration.ofSeconds(10))
+                .untilAsserted(
+                        () -> {
+                            Container.ExecResult result =
+                                    snmpAgent.execInContainer("cat", 
"/tmp/snmp-set.txt");
+                            Assertions.assertEquals(0, result.getExitCode(), 
result.getStderr());
+                            Assertions.assertEquals(EXPECTED_SET, 
result.getStdout().trim());
+                        });
+    }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/src/test/resources/fake_to_snmp.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/src/test/resources/fake_to_snmp.conf
new file mode 100644
index 0000000000..71d2beba28
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-snmp-e2e/src/test/resources/fake_to_snmp.conf
@@ -0,0 +1,50 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+env {
+  parallelism = 1
+  job.mode = "BATCH"
+}
+
+source {
+  FakeSource {
+    row.num = 1
+    schema = {
+      fields {
+        oid = string
+        value = string
+        value_type = string
+      }
+    }
+    rows = [
+      {
+        kind = INSERT
+        fields = ["1.3.6.1.2.1.1.4.0", "seatunnel-e2e", "OctetString"]
+      }
+    ]
+  }
+}
+
+sink {
+  SNMP {
+    host = "snmp-agent"
+    port = 1161
+    community = "e2e-community"
+    timeout_millis = 5000
+    retries = 0
+  }
+}
diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml
index eebf1fce58..fa8c1d20bb 100644
--- a/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml
+++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml
@@ -73,6 +73,7 @@
         <module>connector-druid-e2e</module>
         <module>connector-google-firestore-e2e</module>
         <module>connector-google-pubsub-e2e</module>
+        <module>connector-snmp-e2e</module>
         <module>connector-azure-queue-storage-e2e</module>
         <module>connector-rocketmq-e2e</module>
         <!--        <module>connector-file-obs-e2e</module>-->

Reply via email to