Skip to content

Merge upstream v2.17.0#5

Merged
GT-610 merged 28 commits intolollipopkit:masterfrom
GT-610:merge-v2.17.0
Mar 31, 2026
Merged

Merge upstream v2.17.0#5
GT-610 merged 28 commits intolollipopkit:masterfrom
GT-610:merge-v2.17.0

Conversation

@GT-610
Copy link
Copy Markdown
Collaborator

@GT-610 GT-610 commented Mar 30, 2026

Summary by CodeRabbit

  • 新功能
    • 本地动态端口转发(SOCKS5 NO AUTH + CONNECT),可启动本地 SOCKS5 代理并配置握手/连接超时与最大并发,支持连接过滤回调。
  • 改进
    • 针对 Web/WASM 明确说明原生 TCP 不可用并调整运行时导入以使用 Web 替代实现;传输层新增可选 AES‑GCM(aes128/256‑gcm@openssh.com)AEAD 路径(需显式启用),chacha20‑poly1305 标注为待支持。
  • 文档
    • README 补充动态转发示例、配置说明与验证片段,更新支持算法列表与使用注意。
  • 测试 / 变更日志
    • 增加多项单元与集成测试,发布版本更新为 2.17.0 并补充变更记录。

vicajilau and others added 21 commits March 24, 2026 15:41
…eb-compat-socket-import

fix(web): make SSHSocket conditional import wasm-compatible
…c-forward-socks5

feat: add dynamic SOCKS5 forwarding API
…26-aead-groundwork

feat: start AEAD support with AES-GCM groundwork
@GT-610 GT-610 changed the title Merge v2.17.0 Merge upstream v2.17.0 Mar 30, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71e16a2b-bddf-4bf3-b06e-3bcd489b70f5

📥 Commits

Reviewing files that changed from the base of the PR and between 20eb905 and 5e3db54.

📒 Files selected for processing (1)
  • lib/src/dynamic_forward_io.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/src/dynamic_forward_io.dart

📝 Walkthrough

Walkthrough

将包版本从 2.16.0 升级到 2.17.0。新增本地 SOCKS5 动态转发(仅支持 SOCKS5 NO AUTH + CONNECT、依赖 dart:io):在 API 层新增 SSHClient.forwardDynamicSSHDynamicForwardSSHDynamicForwardOptionsSSHDynamicConnectionFilter,并提供 IO 实现与非 IO 平台 stub、示例与测试。传输层重构以引入 AEAD 发送/接收路径、显式 AEAD 元数据(isAead/aeadTagSize/ivSize/blockSize)、AES‑GCM 算法条目并调整密钥/IV 管理与完整性报告;同时修正 SSHSocket 条件导入映射并更新 README 与 CHANGELOG。

Possibly related PRs

  • Sync with upstream #1:对 AEAD/密码类型与传输层 AEAD 路径的修改,与本次对 SSHCipherType 元数据和 ssh_transport AEAD 实现的改动在代码层面直接重叠。
  • Merge upstream v2.16.0 #4:修改/澄清 SSHSocket 在 Web/IO 平台上的处理与文档,和本次变更中对 SSHSocket 条件导入映射调整存在直接关联。
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确反映了此 PR 的主要目标:合并上游版本 v2.17.0。标题简洁明了,涵盖了所有更改的核心意图。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

refactor: Renamed MAC protection checks to integrity protection checks
test: Updated AEAD tests to use new variable names
fix: Fixed the condition for the dynamic forwarding connection limit
docs: Updated unit test comments to clarify their purpose
chore: Removed duplicate entries from the CHANGELOG
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

…onnection is closed

Fixed an issue in the _SocksConnection class where remote resources were not properly released when the connection was closed
Corrected an incorrect assignment of the client and server IV derivation keys in SSHTransport
Optimized signal handling and closure logic in the sample code
coderabbitai[bot]

This comment was marked as resolved.

Fixed an issue with resource release when a connection is closed during dynamic forwarding, ensuring that both the remote connection and the client are properly closed
Fixed logic for IV derivation errors and integrity checks during SSH transmission
Added a connection closure status flag to prevent duplicate closures
Optimized the key renegotiation mechanism to trigger based on data volume
devin-ai-integration[bot]

This comment was marked as resolved.

…t handling

- In the _SocksConnection class, extract duplicate closure logic into a dedicated `close` method
- In the SSHTransport class, refactor the AEAD encryption logic to support the ChaCha20-Poly1305 algorithm
- Move the re-key trigger logic to the AEAD processing branch
coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

- Adjust code formatting and improve line breaks in long lines to enhance readability
- Extract EOF handling logic into separate methods: _handleClientEOF and _handleRemoteEOF
- Add a maximum handshake size limit to _ByteBuffer
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
lib/src/ssh_transport.dart (1)

1742-1749: ⚠️ Potential issue | 🟠 Major

hasIntegrityProtection 应检查双向完整性保护

当前实现(Line 1743-1746)使用 OR 逻辑,只要任一方向的 AEAD 密钥非空就返回 true。这可能在密钥交换中间状态(仅本地或仅远端密钥已初始化)时返回 true,误导调用者认为双向完整性保护已就绪。

🔧 建议修复:检查双向保护
   bool get hasIntegrityProtection {
-    final usingAead = (_localAeadKey != null ||
-        _localChaChaEncKey != null ||
-        _remoteAeadKey != null ||
-        _remoteChaChaEncKey != null);
-    if (usingAead) return true;
+    final localAeadReady =
+        _localAeadKey != null || _localChaChaEncKey != null;
+    final remoteAeadReady =
+        _remoteAeadKey != null || _remoteChaChaEncKey != null;
+    if (localAeadReady && remoteAeadReady) return true;
     return _localMac != null && _remoteMac != null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/ssh_transport.dart` around lines 1742 - 1749, The current
hasIntegrityProtection returns true if any AEAD/ChaCha key exists locally or
remotely; change it to require both directions be present: compute
usingAeadLocal = (_localAeadKey != null || _localChaChaEncKey != null) and
usingAeadRemote = (_remoteAeadKey != null || _remoteChaChaEncKey != null) and
only return true for AEAD when usingAeadLocal && usingAeadRemote; otherwise fall
back to requiring both _localMac != null && _remoteMac != null for MAC-based
integrity. Update the hasIntegrityProtection getter to check these paired
conditions (referencing hasIntegrityProtection, _localAeadKey,
_localChaChaEncKey, _remoteAeadKey, _remoteChaChaEncKey, _localMac, _remoteMac).
🧹 Nitpick comments (3)
lib/src/ssh_transport.dart (3)

780-789: 发送与接收路径使用不同的密钥字段

_sendAeadPacket(Line 417-418)使用 _localCipherKey/_localIV,但 _consumeAeadPacket(Line 783-784)使用 _remoteAeadKey/_remoteAeadFixedNonce

虽然这两组字段在 _applyLocalKeys/_applyRemoteKeys 中被设置为相同的值,但命名不一致会增加维护难度和引入 bug 的风险。

♻️ 建议统一使用一组字段
     try {
       plaintext = _processAead(
-        key: _remoteAeadKey!,
-        iv: _remoteAeadFixedNonce!,
+        key: _remoteCipherKey!,
+        iv: _remoteIV!,
         sequence: _remotePacketSN.value,
         aad: aad,
         input: encryptedInput,
         forEncryption: false,
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/ssh_transport.dart` around lines 780 - 789, Unify the AEAD key/IV
field names so send and receive use the same identifiers: change uses of
_remoteAeadKey/_remoteAeadFixedNonce in _consumeAeadPacket to the same canonical
fields used by _sendAeadPacket (or vice versa), and update
_applyLocalKeys/_applyRemoteKeys to set that single pair (e.g., a single
_aeadKey and _aeadNonce/_iv) instead of separate _local* and _remote* names;
ensure all references in _sendAeadPacket, _consumeAeadPacket, and any other
crypto helpers (_processAead, sequence handling) are updated accordingly to
avoid mismatch.

439-466: _processAead 每次调用创建新的 cipher 实例

功能正确,但每次发送/接收 AEAD 包都会创建新的 GCMBlockCipher(AESEngine()) 实例。对于高吞吐量场景,可以考虑复用 cipher 实例(仅重新初始化 nonce)以减少分配开销。

_nonceForSequence 的 nonce 构造方式(将序列号加到 IV 的后 8 字节)符合 OpenSSH AES-GCM 规范。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/ssh_transport.dart` around lines 439 - 466, _processAead currently
allocates a new GCMBlockCipher(AESEngine()) on every call causing unnecessary
allocations; change this by promoting a reusable GCMBlockCipher (or separate
encrypt/decrypt cipher instances) to a surrounding class field and reuse it
across calls, reinitializing it with cipher.init(forEncryption,
AEADParameters(KeyParameter(key), 128, _nonceForSequence(iv, sequence), aad))
each time instead of creating a new instance; keep using _nonceForSequence to
build the nonce and ensure you reinit the same cipher instance before processing
input in _processAead to avoid per-call allocation.

633-639: 冗余的 AEAD 重定向检查

此处的 AEAD 检查与 _consumePacket(Line 594-601)中的检查重复。当执行到 _consumeEncryptedPacket 时,按理说 _consumePacket 中的 AEAD 路径条件不满足,但这里又检查了类似的条件。

此外,检查条件使用 _remoteCipherKey/_remoteIV,但 _consumeAeadPacket 内部(Line 783-784)使用的是 _remoteAeadKey/_remoteAeadFixedNonce。虽然在 _applyRemoteKeys 中两组字段被设置为相同的值,但这种不一致容易引起混淆。

建议统一使用一组字段,或添加注释说明两组字段的关系。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/ssh_transport.dart` around lines 633 - 639, The AEAD redirect check
in _consumeEncryptedPacket duplicates the check in _consumePacket and mixes
_remoteCipherKey/_remoteIV with the AEAD-specific fields used inside
_consumeAeadPacket; to fix, either remove the redundant AEAD branch from
_consumeEncryptedPacket so AEAD packets are only routed by _consumePacket, or
change the condition to test the AEAD-specific fields used by _consumeAeadPacket
(use _remoteAeadKey and _remoteAeadFixedNonce instead of
_remoteCipherKey/_remoteIV), and update or add a short comment in
_applyRemoteKeys and near the check explaining the relationship if both field
sets are intentionally kept.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@lib/src/ssh_transport.dart`:
- Around line 1742-1749: The current hasIntegrityProtection returns true if any
AEAD/ChaCha key exists locally or remotely; change it to require both directions
be present: compute usingAeadLocal = (_localAeadKey != null ||
_localChaChaEncKey != null) and usingAeadRemote = (_remoteAeadKey != null ||
_remoteChaChaEncKey != null) and only return true for AEAD when usingAeadLocal
&& usingAeadRemote; otherwise fall back to requiring both _localMac != null &&
_remoteMac != null for MAC-based integrity. Update the hasIntegrityProtection
getter to check these paired conditions (referencing hasIntegrityProtection,
_localAeadKey, _localChaChaEncKey, _remoteAeadKey, _remoteChaChaEncKey,
_localMac, _remoteMac).

---

Nitpick comments:
In `@lib/src/ssh_transport.dart`:
- Around line 780-789: Unify the AEAD key/IV field names so send and receive use
the same identifiers: change uses of _remoteAeadKey/_remoteAeadFixedNonce in
_consumeAeadPacket to the same canonical fields used by _sendAeadPacket (or vice
versa), and update _applyLocalKeys/_applyRemoteKeys to set that single pair
(e.g., a single _aeadKey and _aeadNonce/_iv) instead of separate _local* and
_remote* names; ensure all references in _sendAeadPacket, _consumeAeadPacket,
and any other crypto helpers (_processAead, sequence handling) are updated
accordingly to avoid mismatch.
- Around line 439-466: _processAead currently allocates a new
GCMBlockCipher(AESEngine()) on every call causing unnecessary allocations;
change this by promoting a reusable GCMBlockCipher (or separate encrypt/decrypt
cipher instances) to a surrounding class field and reuse it across calls,
reinitializing it with cipher.init(forEncryption,
AEADParameters(KeyParameter(key), 128, _nonceForSequence(iv, sequence), aad))
each time instead of creating a new instance; keep using _nonceForSequence to
build the nonce and ensure you reinit the same cipher instance before processing
input in _processAead to avoid per-call allocation.
- Around line 633-639: The AEAD redirect check in _consumeEncryptedPacket
duplicates the check in _consumePacket and mixes _remoteCipherKey/_remoteIV with
the AEAD-specific fields used inside _consumeAeadPacket; to fix, either remove
the redundant AEAD branch from _consumeEncryptedPacket so AEAD packets are only
routed by _consumePacket, or change the condition to test the AEAD-specific
fields used by _consumeAeadPacket (use _remoteAeadKey and _remoteAeadFixedNonce
instead of _remoteCipherKey/_remoteIV), and update or add a short comment in
_applyRemoteKeys and near the check explaining the relationship if both field
sets are intentionally kept.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6b15ad4-c55a-47e2-8b7c-56db89ad6953

📥 Commits

Reviewing files that changed from the base of the PR and between f846249 and 9bd5f6e.

📒 Files selected for processing (6)
  • lib/src/dynamic_forward_io.dart
  • lib/src/sftp/sftp_client.dart
  • lib/src/ssh_agent.dart
  • lib/src/ssh_key_pair.dart
  • lib/src/ssh_transport.dart
  • test/src/algorithm/ssh_cipher_type_test.dart
✅ Files skipped from review due to trivial changes (3)
  • lib/src/ssh_agent.dart
  • lib/src/ssh_key_pair.dart
  • lib/src/sftp/sftp_client.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/src/algorithm/ssh_cipher_type_test.dart
  • lib/src/dynamic_forward_io.dart

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Remove the _localAeadFixedNonce variable and related AEAD encryption logic, which are no longer in use

Simplify the conditional logic in the hasIntegrityProtection method
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
lib/src/ssh_transport.dart (2)

239-269: ⚠️ Potential issue | 🔴 Critical

NEW_KEYS 前仍然在用“下一轮协商结果”驱动当前收发路径。

这条和之前的 rekey state 问题是同一个根因:localCipherType / ct / macType 还是直接读 _clientCipherType_serverCipherType_clientMacType_serverMacType,而这些值在收到 KEXINIT 时就已经被下一轮结果覆盖了。当前会话还没切到新 key 时,只要 rekey 跨了 AEAD/非 AEAD 或 ETM/非 ETM,KEX 包就会被按错误路径编码/解码:例如 AEAD → CTR 会落到明文分支,CTR+ETM → GCM(这里现在允许 *_MacType == null)又会让接收侧继续走旧的加密分支,但拿到和活动状态不一致的 macType。另外,_applyLocalKeys() / _applyRemoteKeys() 现在也没有把“另一类”AEAD 状态完整清空,后续 rekey 还会被残留的 ChaCha/GCM key material 继续放大这个误判。这里还是要把 negotiated state 和 active state 分离,只在 NEW_KEYS 后切换活动 cipher/MAC。

Also applies to: 543-556, 988-1105, 1446-1450

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/ssh_transport.dart` around lines 239 - 269, 当前代码在发送/接收数据时直接读取
`_clientCipherType` / `_serverCipherType` / `_clientMacType` / `_serverMacType`
等“协商结果”字段(见 localCipherType 计算与使用处),导致在收到 KEXINIT 到处理 NEW_KEYS
之前会错误地用下一轮协商结果编码/解码包;请把“已协商但未激活”的状态与“活动中”的状态分离:在发送/接收路径(例如使用 localCipherType
的分支)只读活动变量(引入或使用诸如
_activeClientCipherType/_activeServerCipherType、_activeClientMacType/_activeServerMacType),不直接访问
`_client*`/`_server*`;并在 `_applyLocalKeys()` / `_applyRemoteKeys()` 中在切换到
NEW_KEYS 时才更新这些活动变量,同时完整清除与另一类 AEAD(如 ChaCha/GCM)的残留 key material(例如
`_localChaChaEncKey`/`_localChaChaLenKey` /相关 GCM keys)以避免残留影响后续 rekey。

731-748: ⚠️ Potential issue | 🟠 Major

先拦住 paddingLength >= packetLength,否则畸形 AEAD 包会抛出非 SSHError 异常。

这里和 ETM 分支不一样,没有在 paddingLength 越界时提前转成 SSHPacketError。一旦对端发来认证通过但格式非法的 AEAD 包,payloadLength 会变成负数,后面的 Uint8List.sublistView 会抛 RangeError_onSocketData() 不会按 SSH 错误路径处理它。建议把这个上界检查补到这里,或者下沉到 _verifyPacketPadding(),这样 ChaCha 分支也能一起复用。

🔧 建议修复
     final paddingLength = plaintext[0];
+    if (paddingLength >= packetLength) {
+      throw SSHPacketError(
+        'Padding length too large: $paddingLength (packet length is $packetLength)',
+      );
+    }
     final payloadLength = packetLength - paddingLength - 1;
     _verifyPacketPadding(payloadLength, paddingLength);
     return Uint8List.sublistView(plaintext, 1, 1 + payloadLength);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/ssh_transport.dart` around lines 731 - 748, The AEAD branch must
reject malformed packets where paddingLength >= packetLength before computing
payloadLength to avoid a RangeError (which bypasses SSH error handling); add an
explicit upper-bound check after reading paddingLength in the AEAD path that
throws SSHPacketError (same as the ETM branch) when paddingLength >=
packetLength, or move that validation into _verifyPacketPadding so both the AEAD
code path (the block using _processAead and Uint8List.sublistView) and the
ChaCha branch reuse the same check; ensure the thrown error type is
SSHPacketError so _onSocketData() follows the SSH error path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@lib/src/ssh_transport.dart`:
- Around line 239-269: 当前代码在发送/接收数据时直接读取 `_clientCipherType` /
`_serverCipherType` / `_clientMacType` / `_serverMacType` 等“协商结果”字段(见
localCipherType 计算与使用处),导致在收到 KEXINIT 到处理 NEW_KEYS
之前会错误地用下一轮协商结果编码/解码包;请把“已协商但未激活”的状态与“活动中”的状态分离:在发送/接收路径(例如使用 localCipherType
的分支)只读活动变量(引入或使用诸如
_activeClientCipherType/_activeServerCipherType、_activeClientMacType/_activeServerMacType),不直接访问
`_client*`/`_server*`;并在 `_applyLocalKeys()` / `_applyRemoteKeys()` 中在切换到
NEW_KEYS 时才更新这些活动变量,同时完整清除与另一类 AEAD(如 ChaCha/GCM)的残留 key material(例如
`_localChaChaEncKey`/`_localChaChaLenKey` /相关 GCM keys)以避免残留影响后续 rekey。
- Around line 731-748: The AEAD branch must reject malformed packets where
paddingLength >= packetLength before computing payloadLength to avoid a
RangeError (which bypasses SSH error handling); add an explicit upper-bound
check after reading paddingLength in the AEAD path that throws SSHPacketError
(same as the ETM branch) when paddingLength >= packetLength, or move that
validation into _verifyPacketPadding so both the AEAD code path (the block using
_processAead and Uint8List.sublistView) and the ChaCha branch reuse the same
check; ensure the thrown error type is SSHPacketError so _onSocketData() follows
the SSH error path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a83254d8-78f1-414e-a20e-e8ca7b13f76f

📥 Commits

Reviewing files that changed from the base of the PR and between 9bd5f6e and 20eb905.

📒 Files selected for processing (2)
  • lib/src/dynamic_forward_io.dart
  • lib/src/ssh_transport.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/src/dynamic_forward_io.dart

The condition for checking the number of connections should be “less than or equal to” rather than “less than”

Added the _dialing flag to prevent concurrent dialing requests
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 16 additional findings in Devin Review.

Open in Devin Review

Comment on lines +194 to +195
final target = _parseConnectRequest();
if (target == null) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 _dialing flag never reset when SOCKS5 CONNECT request data is incomplete, permanently blocking the connection

In _consumeHandshake(), when the SOCKS5 greeting is parsed successfully and the state transitions to request, the code sets _dialing = true (line 192) before calling _parseConnectRequest() (line 194). If _parseConnectRequest() returns null because the CONNECT request data hasn't arrived yet (which is the normal case — SOCKS5 clients send the greeting, wait for the method selection reply, then send the CONNECT in a separate TCP segment), the method returns at line 195 without resetting _dialing. When the CONNECT request data arrives in a subsequent _onClientData call, _consumeHandshake() hits if (_dialing) return; at line 191 and returns immediately, so the request is never processed. The connection hangs until the handshake timer expires (default 10 seconds), making the dynamic forwarding feature non-functional for all standard SOCKS5 clients.

Suggested change
final target = _parseConnectRequest();
if (target == null) return;
final target = _parseConnectRequest();
if (target == null) {
_dialing = false;
return;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@GT-610 GT-610 merged commit 0dfe0f8 into lollipopkit:master Mar 31, 2026
1 of 2 checks passed
@GT-610 GT-610 deleted the merge-v2.17.0 branch March 31, 2026 07:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants