| /* |
| * Copyright (c) 2026 Project CHIP Authors |
| * All rights reserved. |
| * |
| * Licensed 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. |
| * |
| */ |
| |
| #include "ThreadMeshcopCommissionProxy.h" |
| |
| #include <lib/core/CHIPEncoding.h> |
| #include <lib/dnssd/TxtFields.h> |
| #include <lib/dnssd/minimal_mdns/core/QNameString.h> // nogncheck |
| #include <lib/support/CHIPMemString.h> |
| #include <lib/support/CodeUtils.h> |
| #include <lib/support/logging/CHIPLogging.h> |
| #include <transport/raw/MessageHeader.h> |
| |
| #include <errno.h> |
| #include <inttypes.h> |
| #include <netinet/in.h> |
| #include <sys/socket.h> |
| #include <unistd.h> |
| |
| #include <chrono> |
| #include <thread> |
| |
| using namespace chip; |
| |
| namespace { |
| /** |
| * Internal OT Commissioner Logger implementation. |
| */ |
| class CommissionerLogger : public ot::commissioner::Logger |
| { |
| public: |
| void Log(ot::commissioner::LogLevel level, const std::string & region, const std::string & message) override |
| { |
| ChipLogProgress(Controller, "[ot-commissioner][%u][%s] %s", static_cast<unsigned>(level), region.c_str(), message.c_str()); |
| } |
| }; |
| |
| constexpr char kMatterCServiceSuffix[] = "_matterc._udp.local"; |
| |
| uint64_t JoinerIdFromBytes(const std::vector<uint8_t> & bytes) |
| { |
| const uint8_t * buffer = bytes.data(); |
| return Encoding::BigEndian::Read64(buffer); |
| } |
| |
| std::vector<uint8_t> DiscoveryCodeToVector(Thread::DiscoveryCode code) |
| { |
| uint8_t bytes[sizeof(uint64_t)]; |
| Encoding::BigEndian::Put64(bytes, code.AsUInt64()); |
| return std::vector<uint8_t>(bytes, bytes + sizeof(bytes)); |
| } |
| } // namespace |
| |
| namespace chip { |
| namespace Controller { |
| |
| ThreadMeshcopCommissionProxy::ThreadMeshcopCommissionProxy() : mState(State::kConnecting), mPromiseFulfilled(false) |
| { |
| mCommissioner = ot::commissioner::Commissioner::Create(*this); |
| } |
| |
| ThreadMeshcopCommissionProxy::~ThreadMeshcopCommissionProxy() |
| { |
| std::lock_guard<std::recursive_mutex> lock(mMutex); |
| if (mProxyFd != -1) |
| { |
| if (shutdown(mProxyFd, SHUT_RDWR) == 0 || errno != EBADF) |
| { |
| close(mProxyFd); |
| } |
| |
| mProxyFd = -1; |
| } |
| |
| if (mProxyThread.joinable()) |
| { |
| mProxyThread.join(); |
| } |
| } |
| |
| void ThreadMeshcopCommissionProxy::SetState(State state) |
| { |
| mState = state; |
| } |
| |
| void ThreadMeshcopCommissionProxy::OnHeader(mdns::Minimal::ConstHeaderRef & header) |
| { |
| ChipLogDetail(Controller, "mDNS Response: ID=%u, Answers=%u, Additional=%u", header.GetMessageId(), header.GetAnswerCount(), |
| header.GetAdditionalCount()); |
| } |
| |
| void ThreadMeshcopCommissionProxy::OnQuery(const mdns::Minimal::QueryData & data) |
| { |
| if (mState != State::kDiscovering) |
| { |
| ChipLogProgress(Controller, "Received mDNS query but proxy is not in discovery state"); |
| } |
| |
| ChipLogDetail(Controller, "mDNS query: %s", mdns::Minimal::QNameString(data.GetName()).c_str()); |
| mNodeData.Set<Dnssd::CommissionNodeData>(); |
| } |
| |
| void ThreadMeshcopCommissionProxy::OnResource(mdns::Minimal::ResourceType section, const mdns::Minimal::ResourceData & data) |
| { |
| if (mState != State::kDiscovering) |
| { |
| return; |
| } |
| |
| auto name = mdns::Minimal::QNameString(data.GetName()); |
| auto & commissionData = mNodeData.Get<Dnssd::CommissionNodeData>(); |
| |
| commissionData.threadMeshcop = true; |
| |
| switch (data.GetType()) |
| { |
| case mdns::Minimal::QType::A: |
| case mdns::Minimal::QType::AAAA: |
| Platform::CopyString(commissionData.hostName, name.c_str()); |
| break; |
| |
| case mdns::Minimal::QType::SRV: { |
| mdns::Minimal::SrvRecord srv; |
| if (!srv.Parse(data.GetData(), mDnsPacket)) |
| { |
| ChipLogError(Controller, "Failed to parse mDNS SRV record"); |
| return; |
| } |
| |
| if (!name.EndsWith(kMatterCServiceSuffix)) |
| { |
| ChipLogDetail(Controller, "Ignoring non-Matter service: %s", name.c_str()); |
| return; |
| } |
| |
| // Extract the instance label (portion before "._matterc._udp.local") for CommissionNodeData::instanceName. |
| std::string fullName(name.c_str()); |
| constexpr size_t kMatterCServiceSuffixLen = sizeof(kMatterCServiceSuffix) - 1; // exclude null terminator |
| if (fullName.length() >= kMatterCServiceSuffixLen) |
| { |
| fullName.erase(fullName.length() - kMatterCServiceSuffixLen); |
| } |
| Platform::CopyString(commissionData.instanceName, fullName.c_str()); |
| |
| mServicePort = srv.GetPort(); |
| |
| if (mProxyFd == -1) |
| { |
| CHIP_ERROR err = CreateProxySocket(commissionData); |
| if (err != CHIP_NO_ERROR) |
| { |
| ChipLogError(Controller, "Failed to setup proxy socket: %" CHIP_ERROR_FORMAT, err.Format()); |
| SetState(State::kAborted); |
| } |
| } |
| break; |
| } |
| |
| case mdns::Minimal::QType::TXT: |
| mdns::Minimal::ParseTxtRecord(data.GetData(), this); |
| break; |
| |
| default: |
| break; |
| } |
| } |
| |
| CHIP_ERROR ThreadMeshcopCommissionProxy::CreateProxySocket(chip::Dnssd::CommissionNodeData & commissionData) |
| { |
| mProxyFd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); |
| VerifyOrReturnError(mProxyFd >= 0, CHIP_ERROR_POSIX(errno)); |
| |
| sockaddr_in6 addr = {}; |
| addr.sin6_family = AF_INET6; |
| addr.sin6_port = 0; |
| addr.sin6_addr = in6addr_loopback; |
| |
| if (bind(mProxyFd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) != 0) |
| { |
| close(mProxyFd); |
| mProxyFd = -1; |
| return CHIP_ERROR_POSIX(errno); |
| } |
| |
| socklen_t addr_len = sizeof(addr); |
| if (getsockname(mProxyFd, reinterpret_cast<struct sockaddr *>(&addr), &addr_len) == -1) |
| { |
| close(mProxyFd); |
| mProxyFd = -1; |
| return CHIP_ERROR_POSIX(errno); |
| } |
| |
| commissionData.numIPs = 1; |
| commissionData.port = ntohs(addr.sin6_port); |
| commissionData.ipAddress[0] = Inet::IPAddress::FromSockAddr(addr); |
| commissionData.interfaceId = Inet::InterfaceId::FromIPAddress(commissionData.ipAddress[0]); |
| |
| ChipLogProgress(Controller, "Proxy socket created on port %u", commissionData.port); |
| return CHIP_NO_ERROR; |
| } |
| |
| void ThreadMeshcopCommissionProxy::OnRecord(const mdns::Minimal::BytesRange & name, const mdns::Minimal::BytesRange & value) |
| { |
| ByteSpan key(name.Start(), name.Size()); |
| ByteSpan val(value.Start(), value.Size()); |
| |
| Dnssd::FillNodeDataFromTxt(key, val, mNodeData.Get<Dnssd::CommissionNodeData>()); |
| } |
| |
| void ThreadMeshcopCommissionProxy::ProcessAnnouncement(const std::vector<uint8_t> & joinerIdBytes, uint16_t joinerPort, |
| const std::vector<uint8_t> & payload) |
| { |
| std::lock_guard<std::recursive_mutex> lock(mMutex); |
| |
| if (mPromiseFulfilled) |
| { |
| return; |
| } |
| |
| mNodeData.Set<Dnssd::CommissionNodeData>(); |
| mDnsPacket = mdns::Minimal::BytesRange(payload.data(), payload.data() + payload.size()); |
| |
| if (!mdns::Minimal::ParsePacket(mDnsPacket, this)) |
| { |
| ChipLogError(Controller, "Failed to parse joiner mDNS announcement"); |
| return; |
| } |
| |
| uint32_t discoveredDiscriminator = mNodeData.Get<Dnssd::CommissionNodeData>().longDiscriminator; |
| ChipLogProgress(Controller, "Discovered joiner with discriminator: %u", discoveredDiscriminator); |
| |
| if (!mExpectedDiscriminator.MatchesLongDiscriminator(static_cast<uint16_t>(discoveredDiscriminator))) |
| { |
| ChipLogProgress(Controller, "Discriminator mismatch (Expected %u, Got %u). Ignoring announcement.", |
| mExpectedDiscriminator.GetLongValue(), discoveredDiscriminator); |
| return; |
| } |
| |
| mDiscoveredNodePromise.set_value(mNodeData); |
| mPromiseFulfilled = true; |
| |
| SetState(State::kDiscovered); |
| |
| if (mProxyThread.joinable()) |
| { |
| mProxyThread.join(); |
| } |
| |
| mProxyThread = std::thread([id = joinerIdBytes, this]() { |
| struct sockaddr_storage addr; |
| socklen_t len = sizeof(addr); |
| uint8_t buf[chip::detail::kMaxIPPacketSizeBytes]; |
| ssize_t received; |
| |
| while ((received = recvfrom(mProxyFd, buf, sizeof(buf), 0, reinterpret_cast<struct sockaddr *>(&addr), &len)) > 0) |
| { |
| switch (mState) |
| { |
| case State::kDiscovered: { |
| int rval = connect(mProxyFd, reinterpret_cast<struct sockaddr *>(&addr), len); |
| if (rval < 0) |
| { |
| ChipLogError(Controller, "Failed to connect to Matter Commissioner: %s", strerror(errno)); |
| continue; |
| } |
| SetState(State::kCommissioning); |
| FALLTHROUGH; |
| } |
| |
| case State::kCommissioning: { |
| std::vector<uint8_t> pkt(buf, buf + received); |
| |
| auto error = mCommissioner->SendToJoiner(id, mServicePort, pkt); |
| if (error != ot::commissioner::ErrorCode::kNone) |
| { |
| ChipLogError(Controller, "Failed to send packet to joiner: %s", error.GetMessage().c_str()); |
| return; |
| } |
| break; |
| } |
| default: |
| ChipLogError(Controller, "Invalid CommissionProxy state: %d", static_cast<int>(mState.load())); |
| return; |
| } |
| } |
| }); |
| } |
| |
| void ThreadMeshcopCommissionProxy::OnJoinerMessage(const std::vector<uint8_t> & joinerIdBytes, uint16_t joinerPort, |
| const std::vector<uint8_t> & payload) |
| { |
| std::lock_guard<std::recursive_mutex> lock(mMutex); |
| |
| if (joinerIdBytes.size() != sizeof(uint64_t) || mState == State::kAborted) |
| { |
| return; |
| } |
| |
| uint64_t joinerId = JoinerIdFromBytes(joinerIdBytes); |
| ChipLogDetail(Controller, "Message from joiner 0x%" PRIx64 " on port %u", joinerId, joinerPort); |
| |
| if (mJoinerId == 0) |
| { |
| mJoinerId = joinerId; |
| } |
| else if (mJoinerId != joinerId) |
| { |
| ChipLogProgress(Controller, "Ignoring message from unexpected joiner 0x%" PRIx64, joinerId); |
| return; |
| } |
| |
| switch (mState) |
| { |
| case State::kCommissioning: |
| if (mProxyFd != -1) |
| { |
| if (send(mProxyFd, payload.data(), payload.size(), 0) < 0) |
| { |
| ChipLogError(Controller, "Failed to forward packet to local proxy: %s", strerror(errno)); |
| SetState(State::kAborted); |
| } |
| } |
| break; |
| case State::kAborted: |
| break; |
| case State::kConnecting: |
| // First message from joiner is usually the mDNS announcement |
| SetState(State::kDiscovering); |
| FALLTHROUGH; |
| case State::kDiscovering: |
| ProcessAnnouncement(joinerIdBytes, joinerPort, payload); |
| break; |
| case State::kDiscovered: |
| ChipLogProgress(Controller, "WARNING ignore unsolicited messages after joiner is already discovered"); |
| break; |
| } |
| } |
| |
| ot::commissioner::CommissionerDataset ThreadMeshcopCommissionProxy::MakeCommissionerDataset(Thread::DiscoveryCode code) |
| { |
| ot::commissioner::CommissionerDataset dataset; |
| |
| dataset.mJoinerUdpPort = ot::commissioner::kDefaultJoinerUdpPort; |
| dataset.mPresentFlags |= ot::commissioner::CommissionerDataset::kJoinerUdpPortBit; |
| dataset.mPresentFlags &= |
| ~(ot::commissioner::CommissionerDataset::kSessionIdBit | ot::commissioner::CommissionerDataset::kBorderAgentLocatorBit); |
| |
| if (code.IsAny()) |
| { |
| dataset.mSteeringData = std::vector<uint8_t>{ 0xff }; |
| } |
| else |
| { |
| std::vector<uint8_t> steeringData(ot::commissioner::kMaxSteeringDataLength); |
| ot::commissioner::Commissioner::AddJoiner(steeringData, DiscoveryCodeToVector(code)); |
| dataset.mSteeringData = steeringData; |
| } |
| |
| dataset.mPresentFlags |= ot::commissioner::CommissionerDataset::kSteeringDataBit; |
| return dataset; |
| } |
| CHIP_ERROR ThreadMeshcopCommissionProxy::InitializeCommissioner(ByteSpan & pskc) |
| { |
| VerifyOrReturnError(pskc.size() == Thread::kSizePSKc, CHIP_ERROR_INVALID_ARGUMENT); |
| ot::commissioner::Config config; |
| config.mLogger = std::make_shared<CommissionerLogger>(); |
| config.mEnableCcm = false; |
| config.mProxyMode = true; |
| config.mPSKc = std::vector<uint8_t>(pskc.begin(), pskc.end()); |
| |
| auto error = mCommissioner->Init(config); |
| if (error != ot::commissioner::ErrorCode::kNone) |
| { |
| ChipLogError(Controller, "OT Commissioner Init failed: %s", error.GetMessage().c_str()); |
| return CHIP_ERROR_INTERNAL; |
| } |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR ThreadMeshcopCommissionProxy::Discover(ByteSpan & pskc, const Transport::PeerAddress & peerAddr, |
| const Thread::DiscoveryCode code, SetupDiscriminator expectedDiscriminator, |
| Dnssd::DiscoveredNodeData & nodeData, uint16_t timeout) |
| { |
| using ot::commissioner::Error; |
| |
| Error error; |
| |
| // Reset the promise and state for a new discovery session |
| std::future<Dnssd::DiscoveredNodeData> future; |
| { |
| std::lock_guard<std::recursive_mutex> lock(mMutex); |
| mExpectedDiscriminator = expectedDiscriminator; |
| SetState(State::kConnecting); |
| mDiscoveredNodePromise = std::promise<Dnssd::DiscoveredNodeData>(); |
| future = mDiscoveredNodePromise.get_future(); |
| mPromiseFulfilled = false; |
| mJoinerId = 0; |
| } |
| |
| ReturnErrorOnFailure(InitializeCommissioner(pskc)); |
| |
| { |
| std::string id; |
| char host[Inet::IPAddress::kMaxStringLength]; |
| peerAddr.GetIPAddress().ToString(host); |
| |
| ChipLogProgress(Controller, "Petitioning Thread Border Agent at %s:%u", host, peerAddr.GetPort()); |
| error = mCommissioner->Petition(id, std::string(host), peerAddr.GetPort()); |
| if (error != ot::commissioner::ErrorCode::kNone) |
| { |
| ChipLogError(Controller, "Petition failed: %s", error.GetMessage().c_str()); |
| SetState(State::kAborted); |
| return CHIP_ERROR_INTERNAL; |
| } |
| |
| ChipLogProgress(Controller, "Thread Commissioner active with ID: %s", id.c_str()); |
| } |
| |
| error = mCommissioner->SetCommissionerDataset(MakeCommissionerDataset(code)); |
| if (error != ot::commissioner::ErrorCode::kNone) |
| { |
| ChipLogError(Controller, "Failed to set Steering Data: %s", error.GetMessage().c_str()); |
| SetState(State::kAborted); |
| return CHIP_ERROR_INTERNAL; |
| } |
| |
| ChipLogProgress(Controller, "Waiting for mDNS announcement from joiner..."); |
| auto waitDuration = std::chrono::seconds(timeout); |
| if (future.wait_for(waitDuration) == std::future_status::timeout) |
| { |
| ChipLogError(Controller, "Timed out waiting for joiner mDNS announcement after %u seconds", timeout); |
| SetState(State::kAborted); |
| return CHIP_ERROR_TIMEOUT; |
| } |
| nodeData = future.get(); |
| return CHIP_NO_ERROR; |
| } |
| } // namespace Controller |
| } // namespace chip |