Compare commits
48 commits
main
...
gl/cv2-ctl
Author | SHA1 | Date | |
---|---|---|---|
|
fb7b07fc42 | ||
|
1d2130610c | ||
|
0b04f772ef | ||
|
5c73fe9304 | ||
|
2af105000f | ||
|
cadfa0bc4d | ||
|
f2521c8536 | ||
|
d353e0c6fa | ||
|
346731ea5d | ||
|
2ef38d84d1 | ||
|
5630d48a66 | ||
|
b44a9b8b62 | ||
|
f5b0fc6a8f | ||
|
a20bc772f2 | ||
|
010c4b0d64 | ||
|
5998f1497a | ||
|
a2162c01e3 | ||
|
870c221690 | ||
|
87ad848202 | ||
|
b736b6835d | ||
|
91bec01da8 | ||
|
b5c51796b9 | ||
|
724002f142 | ||
|
377a9d6f41 | ||
|
003b4cf876 | ||
|
b12dd19d44 | ||
|
82c6454950 | ||
|
969c0ee6e3 | ||
|
c092b63987 | ||
|
47efee1df4 | ||
|
18a5452de8 | ||
|
c3794ba8d4 | ||
|
75a5b4438b | ||
|
185a3a2c76 | ||
|
3fcef51137 | ||
|
f959c2f4ca | ||
|
b7a6e106fd | ||
|
7dca7fac11 | ||
|
4ef2d4cc8e | ||
|
55bbd2aec6 | ||
|
36b4659f77 | ||
|
0b5666bde2 | ||
|
e1c72e6d51 | ||
|
5799d9a15b | ||
|
d9d58c8bde | ||
|
d34481d830 | ||
|
4920b68d2c | ||
|
5ce3d1e7a1 |
30 changed files with 3451 additions and 2447 deletions
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
|
@ -1,4 +1,7 @@
|
|||
on: [ push ]
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_ubuntu:
|
||||
|
|
3
.github/workflows/validate.yml
vendored
3
.github/workflows/validate.yml
vendored
|
@ -1,4 +1,5 @@
|
|||
on:
|
||||
pull_request:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
|
@ -44,7 +45,7 @@ jobs:
|
|||
sudo ./.github/workflows/validate-linux.sh
|
||||
|
||||
- name: Archive test results
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{github.sha}}-test-results
|
||||
path: "*test-results*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# vim: ft=dockerfile
|
||||
|
||||
FROM debian:bullseye
|
||||
FROM debian:bookworm
|
||||
|
||||
ARG VERSION
|
||||
|
||||
|
@ -9,9 +9,9 @@ RUN mkdir -p /usr/share/zerotier && \
|
|||
curl -o /usr/share/zerotier/tmp.asc "https://download.zerotier.com/contact%40zerotier.com.gpg" && \
|
||||
gpg --no-default-keyring --keyring /usr/share/zerotier/zerotier.gpg --import /usr/share/zerotier/tmp.asc && \
|
||||
rm -f /usr/share/zerotier/tmp.asc && \
|
||||
echo "deb [signed-by=/usr/share/zerotier/zerotier.gpg] http://download.zerotier.com/debian/bullseye bullseye main" > /etc/apt/sources.list.d/zerotier.list
|
||||
echo "deb [signed-by=/usr/share/zerotier/zerotier.gpg] http://download.zerotier.com/debian/bookworm bookworm main" > /etc/apt/sources.list.d/zerotier.list
|
||||
|
||||
RUN apt-get update -qq && apt-get install zerotier-one=${VERSION} curl iproute2 net-tools iputils-ping openssl libssl1.1 -y
|
||||
RUN apt-get update -qq && apt-get install zerotier-one=${VERSION} curl iproute2 net-tools iputils-ping openssl libssl3 -y
|
||||
RUN rm -rf /var/lib/zerotier-one
|
||||
|
||||
COPY entrypoint.sh.release /entrypoint.sh
|
||||
|
|
|
@ -64,6 +64,7 @@ You can control a few settings including the identity used and the authtoken use
|
|||
- `ZEROTIER_API_SECRET`: replaces the `authtoken.secret` before booting and allows you to manage the control socket's authentication key.
|
||||
- `ZEROTIER_IDENTITY_PUBLIC`: the `identity.public` file for zerotier-one. Use `zerotier-idtool` to generate one of these for you.
|
||||
- `ZEROTIER_IDENTITY_SECRET`: the `identity.secret` file for zerotier-one. Use `zerotier-idtool` to generate one of these for you.
|
||||
- `ZEROTIER_LOCAL_CONF`: Sets the the `local.conf` file content for zerotier-one
|
||||
|
||||
### Tips
|
||||
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
ZeroTier Release Notes
|
||||
======
|
||||
|
||||
# 2024-10-23 -- Version 1.14.2
|
||||
|
||||
* Fix for missing entitlement on macOS Sequoia.
|
||||
* Fix for a problem correctly parsing local.conf to enable low bandwidth mode.
|
||||
* Increment versions of some dependent libraries.
|
||||
* Other fixes.
|
||||
|
||||
# 2024-09-12 -- Version 1.14.1
|
||||
|
||||
* Multithreaded packet I/O support! Currently this is just for Linux and must
|
||||
|
|
1843
controller/CV1.cpp
Normal file
1843
controller/CV1.cpp
Normal file
File diff suppressed because it is too large
Load diff
143
controller/CV1.hpp
Normal file
143
controller/CV1.hpp
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright (c)2019 ZeroTier, Inc.
|
||||
*
|
||||
* Use of this software is governed by the Business Source License included
|
||||
* in the LICENSE.TXT file in the project's root directory.
|
||||
*
|
||||
* Change Date: 2026-01-01
|
||||
*
|
||||
* On the date above, in accordance with the Business Source License, use
|
||||
* of this software will be governed by version 2.0 of the Apache License.
|
||||
*/
|
||||
/****/
|
||||
|
||||
#include "DB.hpp"
|
||||
|
||||
#ifdef ZT_CONTROLLER_USE_LIBPQ
|
||||
|
||||
#ifndef ZT_CONTROLLER_CV1_HPP
|
||||
#define ZT_CONTROLLER_CV1_HPP
|
||||
|
||||
#define ZT_CENTRAL_CONTROLLER_COMMIT_THREADS 4
|
||||
|
||||
#include "ConnectionPool.hpp"
|
||||
#include <pqxx/pqxx>
|
||||
|
||||
#include <memory>
|
||||
#include <redis++/redis++.h>
|
||||
|
||||
#include "../node/Metrics.hpp"
|
||||
|
||||
#include "PostgreSQL.hpp"
|
||||
|
||||
|
||||
namespace smeeclient {
|
||||
struct SmeeClient;
|
||||
}
|
||||
|
||||
namespace ZeroTier {
|
||||
|
||||
struct RedisConfig;
|
||||
|
||||
/**
|
||||
* A controller database driver that talks to PostgreSQL
|
||||
*
|
||||
* This is for use with ZeroTier Central. Others are free to build and use it
|
||||
* but be aware that we might change it at any time.
|
||||
*/
|
||||
class CV1 : public DB
|
||||
{
|
||||
public:
|
||||
CV1(const Identity &myId, const char *path, int listenPort, RedisConfig *rc);
|
||||
virtual ~CV1();
|
||||
|
||||
virtual bool waitForReady();
|
||||
virtual bool isReady();
|
||||
virtual bool save(nlohmann::json &record,bool notifyListeners);
|
||||
virtual void eraseNetwork(const uint64_t networkId);
|
||||
virtual void eraseMember(const uint64_t networkId, const uint64_t memberId);
|
||||
virtual void nodeIsOnline(const uint64_t networkId, const uint64_t memberId, const InetAddress &physicalAddress);
|
||||
virtual AuthInfo getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL);
|
||||
|
||||
virtual bool ready() {
|
||||
return _ready == 2;
|
||||
}
|
||||
|
||||
protected:
|
||||
struct _PairHasher
|
||||
{
|
||||
inline std::size_t operator()(const std::pair<uint64_t,uint64_t> &p) const { return (std::size_t)(p.first ^ p.second); }
|
||||
};
|
||||
virtual void _memberChanged(nlohmann::json &old,nlohmann::json &memberConfig,bool notifyListeners) {
|
||||
DB::_memberChanged(old, memberConfig, notifyListeners);
|
||||
}
|
||||
|
||||
virtual void _networkChanged(nlohmann::json &old,nlohmann::json &networkConfig,bool notifyListeners) {
|
||||
DB::_networkChanged(old, networkConfig, notifyListeners);
|
||||
}
|
||||
|
||||
private:
|
||||
void initializeNetworks();
|
||||
void initializeMembers();
|
||||
void heartbeat();
|
||||
void membersDbWatcher();
|
||||
void _membersWatcher_Postgres();
|
||||
void networksDbWatcher();
|
||||
void _networksWatcher_Postgres();
|
||||
|
||||
void _membersWatcher_Redis();
|
||||
void _networksWatcher_Redis();
|
||||
|
||||
void commitThread();
|
||||
void onlineNotificationThread();
|
||||
void onlineNotification_Postgres();
|
||||
void onlineNotification_Redis();
|
||||
uint64_t _doRedisUpdate(sw::redis::Transaction &tx, std::string &controllerId,
|
||||
std::unordered_map< std::pair<uint64_t,uint64_t>,std::pair<int64_t,InetAddress>,_PairHasher > &lastOnline);
|
||||
|
||||
void configureSmee();
|
||||
void notifyNewMember(const std::string &networkID, const std::string &memberID);
|
||||
|
||||
enum OverrideMode {
|
||||
ALLOW_PGBOUNCER_OVERRIDE = 0,
|
||||
NO_OVERRIDE = 1
|
||||
};
|
||||
|
||||
std::shared_ptr<ConnectionPool<PostgresConnection> > _pool;
|
||||
|
||||
const Identity _myId;
|
||||
const Address _myAddress;
|
||||
std::string _myAddressStr;
|
||||
std::string _connString;
|
||||
|
||||
BlockingQueue< std::pair<nlohmann::json,bool> > _commitQueue;
|
||||
|
||||
std::thread _heartbeatThread;
|
||||
std::thread _membersDbWatcher;
|
||||
std::thread _networksDbWatcher;
|
||||
std::thread _commitThread[ZT_CENTRAL_CONTROLLER_COMMIT_THREADS];
|
||||
std::thread _onlineNotificationThread;
|
||||
|
||||
std::unordered_map< std::pair<uint64_t,uint64_t>,std::pair<int64_t,InetAddress>,_PairHasher > _lastOnline;
|
||||
|
||||
mutable std::mutex _lastOnline_l;
|
||||
mutable std::mutex _readyLock;
|
||||
std::atomic<int> _ready, _connected, _run;
|
||||
mutable volatile bool _waitNoticePrinted;
|
||||
|
||||
int _listenPort;
|
||||
uint8_t _ssoPsk[48];
|
||||
|
||||
RedisConfig *_rc;
|
||||
std::shared_ptr<sw::redis::Redis> _redis;
|
||||
std::shared_ptr<sw::redis::RedisCluster> _cluster;
|
||||
bool _redisMemberStatus;
|
||||
|
||||
smeeclient::SmeeClient *_smee;
|
||||
};
|
||||
|
||||
} // namespace ZeroTier
|
||||
|
||||
#endif // ZT_CONTROLLER_CV1_HPP
|
||||
|
||||
#endif // ZT_CONTROLLER_USE_LIBPQ
|
512
controller/CV2.cpp
Normal file
512
controller/CV2.cpp
Normal file
|
@ -0,0 +1,512 @@
|
|||
/*
|
||||
* Copyright (c)2025 ZeroTier, Inc.
|
||||
*
|
||||
* Use of this software is governed by the Business Source License included
|
||||
* in the LICENSE.TXT file in the project's root directory.
|
||||
*
|
||||
* Change Date: 2026-01-01
|
||||
*
|
||||
* On the date above, in accordance with the Business Source License, use
|
||||
* of this software will be governed by version 2.0 of the Apache License.
|
||||
*/
|
||||
/****/
|
||||
|
||||
#include "CV2.hpp"
|
||||
|
||||
#ifdef ZT_CONTROLLER_USE_LIBPQ
|
||||
|
||||
#include "../node/Constants.hpp"
|
||||
#include "../node/SHA512.hpp"
|
||||
#include "EmbeddedNetworkController.hpp"
|
||||
#include "../version.h"
|
||||
#include "CtlUtil.hpp"
|
||||
|
||||
#include <libpq-fe.h>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <climits>
|
||||
#include <chrono>
|
||||
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace {
|
||||
|
||||
}
|
||||
|
||||
using namespace ZeroTier;
|
||||
|
||||
CV2::CV2(const Identity &myId, const char *path, int listenPort)
|
||||
: DB()
|
||||
, _pool()
|
||||
, _myId(myId)
|
||||
, _myAddress(myId.address())
|
||||
, _ready(0)
|
||||
, _connected(1)
|
||||
, _run(1)
|
||||
, _waitNoticePrinted(false)
|
||||
, _listenPort(listenPort)
|
||||
{
|
||||
char myAddress[64];
|
||||
_myAddressStr = myId.address().toString(myAddress);
|
||||
|
||||
// replace cv2: with postgres: for the path/connstring
|
||||
std::string _path(path);
|
||||
if (_path.length() > 4 && _path.substr(0, 4) == "cv2:") {
|
||||
_path = "postgres:" + _path.substr(4);
|
||||
}
|
||||
_connString = std::string(_path);
|
||||
|
||||
auto f = std::make_shared<PostgresConnFactory>(_connString);
|
||||
_pool = std::make_shared<ConnectionPool<PostgresConnection> >(
|
||||
15, 5, std::static_pointer_cast<ConnectionFactory>(f));
|
||||
|
||||
memset(_ssoPsk, 0, sizeof(_ssoPsk));
|
||||
char *const ssoPskHex = getenv("ZT_SSO_PSK");
|
||||
#ifdef ZT_TRACE
|
||||
fprintf(stderr, "ZT_SSO_PSK: %s\n", ssoPskHex);
|
||||
#endif
|
||||
if (ssoPskHex) {
|
||||
// SECURITY: note that ssoPskHex will always be null-terminated if libc actually
|
||||
// returns something non-NULL. If the hex encodes something shorter than 48 bytes,
|
||||
// it will be padded at the end with zeroes. If longer, it'll be truncated.
|
||||
Utils::unhex(ssoPskHex, _ssoPsk, sizeof(_ssoPsk));
|
||||
}
|
||||
|
||||
auto c = _pool->borrow();
|
||||
pqxx::work txn{*c->c};
|
||||
|
||||
pqxx::row r{txn.exec1("SELECT version FROM ztc_database")};
|
||||
int dbVersion = r[0].as<int>();
|
||||
txn.commit();
|
||||
|
||||
// if (dbVersion < DB_MINIMUM_VERSION) {
|
||||
// fprintf(stderr, "Central database schema version too low. This controller version requires a minimum schema version of %d. Please upgrade your Central instance", DB_MINIMUM_VERSION);
|
||||
// exit(1);
|
||||
// }
|
||||
_pool->unborrow(c);
|
||||
|
||||
_readyLock.lock();
|
||||
|
||||
fprintf(stderr, "[%s] NOTICE: %.10llx controller PostgreSQL waiting for initial data download..." ZT_EOL_S, ::_timestr(), (unsigned long long)_myAddress.toInt());
|
||||
_waitNoticePrinted = true;
|
||||
|
||||
initializeNetworks();
|
||||
initializeMembers();
|
||||
|
||||
_heartbeatThread = std::thread(&CV2::heartbeat, this);
|
||||
_membersDbWatcher = std::thread(&CV2::membersDbWatcher, this);
|
||||
_networksDbWatcher = std::thread(&CV2::networksDbWatcher, this);
|
||||
for (int i = 0; i < ZT_CENTRAL_CONTROLLER_COMMIT_THREADS; ++i) {
|
||||
_commitThread[i] = std::thread(&CV2::commitThread, this);
|
||||
}
|
||||
_onlineNotificationThread = std::thread(&CV2::onlineNotificationThread, this);
|
||||
}
|
||||
|
||||
CV2::~CV2()
|
||||
{
|
||||
_run = 0;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
_heartbeatThread.join();
|
||||
_membersDbWatcher.join();
|
||||
_networksDbWatcher.join();
|
||||
_commitQueue.stop();
|
||||
for (int i = 0; i < ZT_CENTRAL_CONTROLLER_COMMIT_THREADS; ++i) {
|
||||
_commitThread[i].join();
|
||||
}
|
||||
_onlineNotificationThread.join();
|
||||
}
|
||||
|
||||
bool CV2::waitForReady()
|
||||
{
|
||||
while (_ready < 2) {
|
||||
_readyLock.lock();
|
||||
_readyLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CV2::isReady()
|
||||
{
|
||||
return (_ready == 2) && _cldemote;
|
||||
}
|
||||
|
||||
bool CV2::save(nlohmann::json &record,bool notifyListeners)
|
||||
{
|
||||
bool modified = false;
|
||||
try {
|
||||
if (!record.is_object()) {
|
||||
fprintf(stderr, "record is not an object?!?\n");
|
||||
return false;
|
||||
}
|
||||
const std::string objtype = record["objtype"];
|
||||
if (objtype == "network") {
|
||||
//fprintf(stderr, "network save\n");
|
||||
const uint64_t nwid = OSUtils::jsonIntHex(record["id"],0ULL);
|
||||
if (nwid) {
|
||||
nlohmann::json old;
|
||||
get(nwid,old);
|
||||
if ((!old.is_object())||(!_compareRecords(old,record))) {
|
||||
record["revision"] = OSUtils::jsonInt(record["revision"],0ULL) + 1ULL;
|
||||
_commitQueue.post(std::pair<nlohmann::json,bool>(record,notifyListeners));
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
} else if (objtype == "member") {
|
||||
std::string networkId = record["nwid"];
|
||||
std::string memberId = record["id"];
|
||||
const uint64_t nwid = OSUtils::jsonIntHex(record["nwid"],0ULL);
|
||||
const uint64_t id = OSUtils::jsonIntHex(record["id"],0ULL);
|
||||
//fprintf(stderr, "member save %s-%s\n", networkId.c_str(), memberId.c_str());
|
||||
if ((id)&&(nwid)) {
|
||||
nlohmann::json network,old;
|
||||
get(nwid,network,id,old);
|
||||
if ((!old.is_object())||(!_compareRecords(old,record))) {
|
||||
//fprintf(stderr, "commit queue post\n");
|
||||
record["revision"] = OSUtils::jsonInt(record["revision"],0ULL) + 1ULL;
|
||||
_commitQueue.post(std::pair<nlohmann::json,bool>(record,notifyListeners));
|
||||
modified = true;
|
||||
} else {
|
||||
//fprintf(stderr, "no change\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "uhh waaat\n");
|
||||
}
|
||||
} catch (std::exception &e) {
|
||||
fprintf(stderr, "Error on PostgreSQL::save: %s\n", e.what());
|
||||
} catch (...) {
|
||||
fprintf(stderr, "Unknown error on PostgreSQL::save\n");
|
||||
}
|
||||
return modified;
|
||||
}
|
||||
|
||||
void CV2::eraseNetwork(const uint64_t networkId)
|
||||
{
|
||||
fprintf(stderr, "PostgreSQL::eraseNetwork\n");
|
||||
char tmp2[24];
|
||||
waitForReady();
|
||||
Utils::hex(networkId, tmp2);
|
||||
std::pair<nlohmann::json,bool> tmp;
|
||||
tmp.first["id"] = tmp2;
|
||||
tmp.first["objtype"] = "_delete_network";
|
||||
tmp.second = true;
|
||||
_commitQueue.post(tmp);
|
||||
nlohmann::json nullJson;
|
||||
_networkChanged(tmp.first, nullJson, true);
|
||||
}
|
||||
|
||||
void CV2::eraseMember(const uint64_t networkId, const uint64_t memberId)
|
||||
{
|
||||
fprintf(stderr, "PostgreSQL::eraseMember\n");
|
||||
char tmp2[24];
|
||||
waitForReady();
|
||||
std::pair<nlohmann::json,bool> tmp, nw;
|
||||
Utils::hex(networkId, tmp2);
|
||||
tmp.first["nwid"] = tmp2;
|
||||
Utils::hex(memberId, tmp2);
|
||||
tmp.first["id"] = tmp2;
|
||||
tmp.first["objtype"] = "_delete_member";
|
||||
tmp.second = true;
|
||||
_commitQueue.post(tmp);
|
||||
nlohmann::json nullJson;
|
||||
_memberChanged(tmp.first, nullJson, true);
|
||||
}
|
||||
|
||||
void CV2::nodeIsOnline(const uint64_t networkId, const uint64_t memberId, const InetAddress &physicalAddress)
|
||||
{
|
||||
std::lock_guard<std::mutex> l(_lastOnline_l);
|
||||
std::pair<int64_t, InetAddress> &i = _lastOnline[std::pair<uint64_t,uint64_t>(networkId, memberId)];
|
||||
i.first = OSUtils::now();
|
||||
if (physicalAddress) {
|
||||
i.second = physicalAddress;
|
||||
}
|
||||
}
|
||||
|
||||
AuthInfo CV2::getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL)
|
||||
{
|
||||
// TODO: Redo this for CV2
|
||||
|
||||
Metrics::db_get_sso_info++;
|
||||
// NONCE is just a random character string. no semantic meaning
|
||||
// state = HMAC SHA384 of Nonce based on shared sso key
|
||||
//
|
||||
// need nonce timeout in database? make sure it's used within X time
|
||||
// X is 5 minutes for now. Make configurable later?
|
||||
//
|
||||
// how do we tell when a nonce is used? if auth_expiration_time is set
|
||||
std::string networkId = member["nwid"];
|
||||
std::string memberId = member["id"];
|
||||
|
||||
|
||||
char authenticationURL[4096] = {0};
|
||||
AuthInfo info;
|
||||
info.enabled = true;
|
||||
|
||||
//if (memberId == "a10dccea52" && networkId == "8056c2e21c24673d") {
|
||||
// fprintf(stderr, "invalid authinfo for grant's machine\n");
|
||||
// info.version=1;
|
||||
// return info;
|
||||
//}
|
||||
// fprintf(stderr, "PostgreSQL::updateMemberOnLoad: %s-%s\n", networkId.c_str(), memberId.c_str());
|
||||
std::shared_ptr<PostgresConnection> c;
|
||||
try {
|
||||
// c = _pool->borrow();
|
||||
// pqxx::work w(*c->c);
|
||||
|
||||
// char nonceBytes[16] = {0};
|
||||
// std::string nonce = "";
|
||||
|
||||
// // check if the member exists first.
|
||||
// pqxx::row count = w.exec_params1("SELECT count(id) FROM ztc_member WHERE id = $1 AND network_id = $2 AND deleted = false", memberId, networkId);
|
||||
// if (count[0].as<int>() == 1) {
|
||||
// // get active nonce, if exists.
|
||||
// pqxx::result r = w.exec_params("SELECT nonce FROM ztc_sso_expiry "
|
||||
// "WHERE network_id = $1 AND member_id = $2 "
|
||||
// "AND ((NOW() AT TIME ZONE 'UTC') <= authentication_expiry_time) AND ((NOW() AT TIME ZONE 'UTC') <= nonce_expiration)",
|
||||
// networkId, memberId);
|
||||
|
||||
// if (r.size() == 0) {
|
||||
// // no active nonce.
|
||||
// // find an unused nonce, if one exists.
|
||||
// pqxx::result r = w.exec_params("SELECT nonce FROM ztc_sso_expiry "
|
||||
// "WHERE network_id = $1 AND member_id = $2 "
|
||||
// "AND authentication_expiry_time IS NULL AND ((NOW() AT TIME ZONE 'UTC') <= nonce_expiration)",
|
||||
// networkId, memberId);
|
||||
|
||||
// if (r.size() == 1) {
|
||||
// // we have an existing nonce. Use it
|
||||
// nonce = r.at(0)[0].as<std::string>();
|
||||
// Utils::unhex(nonce.c_str(), nonceBytes, sizeof(nonceBytes));
|
||||
// } else if (r.empty()) {
|
||||
// // create a nonce
|
||||
// Utils::getSecureRandom(nonceBytes, 16);
|
||||
// char nonceBuf[64] = {0};
|
||||
// Utils::hex(nonceBytes, sizeof(nonceBytes), nonceBuf);
|
||||
// nonce = std::string(nonceBuf);
|
||||
|
||||
// pqxx::result ir = w.exec_params0("INSERT INTO ztc_sso_expiry "
|
||||
// "(nonce, nonce_expiration, network_id, member_id) VALUES "
|
||||
// "($1, TO_TIMESTAMP($2::double precision/1000), $3, $4)",
|
||||
// nonce, OSUtils::now() + 300000, networkId, memberId);
|
||||
|
||||
// w.commit();
|
||||
// } else {
|
||||
// // > 1 ?!? Thats an error!
|
||||
// fprintf(stderr, "> 1 unused nonce!\n");
|
||||
// exit(6);
|
||||
// }
|
||||
// } else if (r.size() == 1) {
|
||||
// nonce = r.at(0)[0].as<std::string>();
|
||||
// Utils::unhex(nonce.c_str(), nonceBytes, sizeof(nonceBytes));
|
||||
// } else {
|
||||
// // more than 1 nonce in use? Uhhh...
|
||||
// fprintf(stderr, "> 1 nonce in use for network member?!?\n");
|
||||
// exit(7);
|
||||
// }
|
||||
|
||||
// r = w.exec_params(
|
||||
// "SELECT oc.client_id, oc.authorization_endpoint, oc.issuer, oc.provider, oc.sso_impl_version "
|
||||
// "FROM ztc_network AS n "
|
||||
// "INNER JOIN ztc_org o "
|
||||
// " ON o.owner_id = n.owner_id "
|
||||
// "LEFT OUTER JOIN ztc_network_oidc_config noc "
|
||||
// " ON noc.network_id = n.id "
|
||||
// "LEFT OUTER JOIN ztc_oidc_config oc "
|
||||
// " ON noc.client_id = oc.client_id AND oc.org_id = o.org_id "
|
||||
// "WHERE n.id = $1 AND n.sso_enabled = true", networkId);
|
||||
|
||||
// std::string client_id = "";
|
||||
// std::string authorization_endpoint = "";
|
||||
// std::string issuer = "";
|
||||
// std::string provider = "";
|
||||
// uint64_t sso_version = 0;
|
||||
|
||||
// if (r.size() == 1) {
|
||||
// client_id = r.at(0)[0].as<std::optional<std::string>>().value_or("");
|
||||
// authorization_endpoint = r.at(0)[1].as<std::optional<std::string>>().value_or("");
|
||||
// issuer = r.at(0)[2].as<std::optional<std::string>>().value_or("");
|
||||
// provider = r.at(0)[3].as<std::optional<std::string>>().value_or("");
|
||||
// sso_version = r.at(0)[4].as<std::optional<uint64_t>>().value_or(1);
|
||||
// } else if (r.size() > 1) {
|
||||
// fprintf(stderr, "ERROR: More than one auth endpoint for an organization?!?!? NetworkID: %s\n", networkId.c_str());
|
||||
// } else {
|
||||
// fprintf(stderr, "No client or auth endpoint?!?\n");
|
||||
// }
|
||||
|
||||
// info.version = sso_version;
|
||||
|
||||
// // no catch all else because we don't actually care if no records exist here. just continue as normal.
|
||||
// if ((!client_id.empty())&&(!authorization_endpoint.empty())) {
|
||||
|
||||
// uint8_t state[48];
|
||||
// HMACSHA384(_ssoPsk, nonceBytes, sizeof(nonceBytes), state);
|
||||
// char state_hex[256];
|
||||
// Utils::hex(state, 48, state_hex);
|
||||
|
||||
// if (info.version == 0) {
|
||||
// char url[2048] = {0};
|
||||
// OSUtils::ztsnprintf(url, sizeof(authenticationURL),
|
||||
// "%s?response_type=id_token&response_mode=form_post&scope=openid+email+profile&redirect_uri=%s&nonce=%s&state=%s&client_id=%s",
|
||||
// authorization_endpoint.c_str(),
|
||||
// url_encode(redirectURL).c_str(),
|
||||
// nonce.c_str(),
|
||||
// state_hex,
|
||||
// client_id.c_str());
|
||||
// info.authenticationURL = std::string(url);
|
||||
// } else if (info.version == 1) {
|
||||
// info.ssoClientID = client_id;
|
||||
// info.issuerURL = issuer;
|
||||
// info.ssoProvider = provider;
|
||||
// info.ssoNonce = nonce;
|
||||
// info.ssoState = std::string(state_hex) + "_" +networkId;
|
||||
// info.centralAuthURL = redirectURL;
|
||||
// #ifdef ZT_DEBUG
|
||||
// fprintf(
|
||||
// stderr,
|
||||
// "ssoClientID: %s\nissuerURL: %s\nssoNonce: %s\nssoState: %s\ncentralAuthURL: %s\nprovider: %s\n",
|
||||
// info.ssoClientID.c_str(),
|
||||
// info.issuerURL.c_str(),
|
||||
// info.ssoNonce.c_str(),
|
||||
// info.ssoState.c_str(),
|
||||
// info.centralAuthURL.c_str(),
|
||||
// provider.c_str());
|
||||
// #endif
|
||||
// }
|
||||
// } else {
|
||||
// fprintf(stderr, "client_id: %s\nauthorization_endpoint: %s\n", client_id.c_str(), authorization_endpoint.c_str());
|
||||
// }
|
||||
// }
|
||||
|
||||
// _pool->unborrow(c);
|
||||
} catch (std::exception &e) {
|
||||
fprintf(stderr, "ERROR: Error updating member on load for network %s: %s\n", networkId.c_str(), e.what());
|
||||
}
|
||||
|
||||
return info; //std::string(authenticationURL);
|
||||
}
|
||||
|
||||
void CV2::initializeNetworks()
|
||||
{
|
||||
try {
|
||||
// TODO: Update for CV2
|
||||
|
||||
if (++this->_ready == 2) {
|
||||
if (_waitNoticePrinted) {
|
||||
fprintf(stderr,"[%s] NOTICE: %.10llx controller PostgreSQL data download complete." ZT_EOL_S,_timestr(),(unsigned long long)_myAddress.toInt());
|
||||
}
|
||||
_readyLock.unlock();
|
||||
}
|
||||
fprintf(stderr, "network init done\n");
|
||||
} catch (std::exception &e) {
|
||||
fprintf(stderr, "ERROR: Error initializing networks: %s\n", e.what());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
|
||||
exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void CV2::initializeMembers()
|
||||
{
|
||||
std::string memberId;
|
||||
std::string networkId;
|
||||
try {
|
||||
// TODO: Update for CV2
|
||||
|
||||
if (++this->_ready == 2) {
|
||||
if (_waitNoticePrinted) {
|
||||
fprintf(stderr,"[%s] NOTICE: %.10llx controller PostgreSQL data download complete." ZT_EOL_S,_timestr(),(unsigned long long)_myAddress.toInt());
|
||||
}
|
||||
_readyLock.unlock();
|
||||
}
|
||||
fprintf(stderr, "member init done\n");
|
||||
} catch (std::exception &e) {
|
||||
fprintf(stderr, "ERROR: Error initializing member: %s-%s %s\n", networkId.c_str(), memberId.c_str(), e.what());
|
||||
exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void CV2::heartbeat()
|
||||
{
|
||||
char publicId[1024];
|
||||
char hostnameTmp[1024];
|
||||
_myId.toString(false,publicId);
|
||||
if (gethostname(hostnameTmp, sizeof(hostnameTmp))!= 0) {
|
||||
hostnameTmp[0] = (char)0;
|
||||
} else {
|
||||
for (int i = 0; i < (int)sizeof(hostnameTmp); ++i) {
|
||||
if ((hostnameTmp[i] == '.')||(hostnameTmp[i] == 0)) {
|
||||
hostnameTmp[i] = (char)0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const char *controllerId = _myAddressStr.c_str();
|
||||
const char *publicIdentity = publicId;
|
||||
const char *hostname = hostnameTmp;
|
||||
|
||||
// TODO: Update for CV2
|
||||
|
||||
while (_run == 1) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(5));
|
||||
}
|
||||
fprintf(stderr, "Exited heartbeat thread\n");
|
||||
}
|
||||
|
||||
void CV2::membersDbWatcher() {
|
||||
auto c = _pool->borrow();
|
||||
|
||||
std::string stream = "member_" + _myAddressStr;
|
||||
|
||||
fprintf(stderr, "Listening to member stream: %s\n", stream.c_str());
|
||||
MemberNotificationReceiver m(this, *c->c, stream);
|
||||
|
||||
while(_run == 1) {
|
||||
c->c->await_notification(5, 0);
|
||||
}
|
||||
|
||||
_pool->unborrow(c);
|
||||
|
||||
fprintf(stderr, "Exited membersDbWatcher\n");
|
||||
}
|
||||
|
||||
void CV2::networksDbWatcher()
|
||||
{
|
||||
std::string stream = "network_" + _myAddressStr;
|
||||
|
||||
fprintf(stderr, "Listening to member stream: %s\n", stream.c_str());
|
||||
|
||||
auto c = _pool->borrow();
|
||||
|
||||
NetworkNotificationReceiver n(this, *c->c, stream);
|
||||
|
||||
while(_run == 1) {
|
||||
c->c->await_notification(5,0);
|
||||
}
|
||||
|
||||
_pool->unborrow(c);
|
||||
fprintf(stderr, "Exited networksDbWatcher\n");
|
||||
}
|
||||
|
||||
void CV2::commitThread()
|
||||
{
|
||||
// TODO: Update for CV2
|
||||
}
|
||||
|
||||
void CV2::onlineNotificationThread() {
|
||||
waitForReady();
|
||||
|
||||
_connected = 1;
|
||||
|
||||
nlohmann::json jtmp1, jtmp2;
|
||||
while (_run == 1) {
|
||||
// TODO: Update for CV2
|
||||
}
|
||||
|
||||
fprintf(stderr, "%s: Fell out of run loop in onlineNotificationThread\n", _myAddressStr.c_str());
|
||||
if (_run == 1) {
|
||||
fprintf(stderr, "ERROR: %s onlineNotificationThread should still be running! Exiting Controller.\n", _myAddressStr.c_str());
|
||||
exit(6);
|
||||
}
|
||||
}
|
||||
#endif // ZT_CONTROLLER_USE_LIBPQ
|
112
controller/CV2.hpp
Normal file
112
controller/CV2.hpp
Normal file
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (c)2025 ZeroTier, Inc.
|
||||
*
|
||||
* Use of this software is governed by the Business Source License included
|
||||
* in the LICENSE.TXT file in the project's root directory.
|
||||
*
|
||||
* Change Date: 2026-01-01
|
||||
*
|
||||
* On the date above, in accordance with the Business Source License, use
|
||||
* of this software will be governed by version 2.0 of the Apache License.
|
||||
*/
|
||||
/****/
|
||||
|
||||
#include "DB.hpp"
|
||||
|
||||
#ifdef ZT_CONTROLLER_USE_LIBPQ
|
||||
|
||||
#ifndef ZT_CONTROLLER_CV2_HPP
|
||||
#define ZT_CONTROLLER_CV2_HPP
|
||||
|
||||
#define ZT_CENTRAL_CONTROLLER_COMMIT_THREADS 4
|
||||
|
||||
#include "ConnectionPool.hpp"
|
||||
#include <pqxx/pqxx>
|
||||
|
||||
#include <memory>
|
||||
#include <redis++/redis++.h>
|
||||
|
||||
#include "../node/Metrics.hpp"
|
||||
|
||||
#include "PostgreSQL.hpp"
|
||||
|
||||
namespace ZeroTier {
|
||||
|
||||
class CV2 : public DB
|
||||
{
|
||||
public:
|
||||
CV2(const Identity &myId, const char *path, int listenPort);
|
||||
virtual ~CV2();
|
||||
|
||||
virtual bool waitForReady();
|
||||
virtual bool isReady();
|
||||
virtual bool save(nlohmann::json &record,bool notifyListeners);
|
||||
virtual void eraseNetwork(const uint64_t networkId);
|
||||
virtual void eraseMember(const uint64_t networkId, const uint64_t memberId);
|
||||
virtual void nodeIsOnline(const uint64_t networkId, const uint64_t memberId, const InetAddress &physicalAddress);
|
||||
virtual AuthInfo getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL);
|
||||
|
||||
virtual bool ready() {
|
||||
return _ready == 2;
|
||||
}
|
||||
|
||||
protected:
|
||||
struct _PairHasher
|
||||
{
|
||||
inline std::size_t operator()(const std::pair<uint64_t,uint64_t> &p) const { return (std::size_t)(p.first ^ p.second); }
|
||||
};
|
||||
virtual void _memberChanged(nlohmann::json &old,nlohmann::json &memberConfig,bool notifyListeners) {
|
||||
DB::_memberChanged(old, memberConfig, notifyListeners);
|
||||
}
|
||||
|
||||
virtual void _networkChanged(nlohmann::json &old,nlohmann::json &networkConfig,bool notifyListeners) {
|
||||
DB::_networkChanged(old, networkConfig, notifyListeners);
|
||||
}
|
||||
|
||||
private:
|
||||
void initializeNetworks();
|
||||
void initializeMembers();
|
||||
void heartbeat();
|
||||
void membersDbWatcher();
|
||||
void networksDbWatcher();
|
||||
|
||||
void commitThread();
|
||||
void onlineNotificationThread();
|
||||
|
||||
// void notifyNewMember(const std::string &networkID, const std::string &memberID);
|
||||
|
||||
enum OverrideMode {
|
||||
ALLOW_PGBOUNCER_OVERRIDE = 0,
|
||||
NO_OVERRIDE = 1
|
||||
};
|
||||
|
||||
std::shared_ptr<ConnectionPool<PostgresConnection> > _pool;
|
||||
|
||||
const Identity _myId;
|
||||
const Address _myAddress;
|
||||
std::string _myAddressStr;
|
||||
std::string _connString;
|
||||
|
||||
BlockingQueue< std::pair<nlohmann::json,bool> > _commitQueue;
|
||||
|
||||
std::thread _heartbeatThread;
|
||||
std::thread _membersDbWatcher;
|
||||
std::thread _networksDbWatcher;
|
||||
std::thread _commitThread[ZT_CENTRAL_CONTROLLER_COMMIT_THREADS];
|
||||
std::thread _onlineNotificationThread;
|
||||
|
||||
std::unordered_map< std::pair<uint64_t,uint64_t>,std::pair<int64_t,InetAddress>,_PairHasher > _lastOnline;
|
||||
|
||||
mutable std::mutex _lastOnline_l;
|
||||
mutable std::mutex _readyLock;
|
||||
std::atomic<int> _ready, _connected, _run;
|
||||
mutable volatile bool _waitNoticePrinted;
|
||||
|
||||
int _listenPort;
|
||||
uint8_t _ssoPsk[48];
|
||||
};
|
||||
|
||||
} // namespace Zerotier
|
||||
|
||||
#endif // ZT_CONTROLLER_CV2_HPP
|
||||
#endif // ZT_CONTROLLER_USE_LIBPQ
|
62
controller/CtlUtil.cpp
Normal file
62
controller/CtlUtil.cpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#include "CtlUtil.hpp"
|
||||
|
||||
#ifdef ZT_CONTROLLER_USE_LIBPQ
|
||||
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
namespace ZeroTier {
|
||||
|
||||
const char *_timestr()
|
||||
{
|
||||
time_t t = time(0);
|
||||
char *ts = ctime(&t);
|
||||
char *p = ts;
|
||||
if (!p)
|
||||
return "";
|
||||
while (*p) {
|
||||
if (*p == '\n') {
|
||||
*p = (char)0;
|
||||
break;
|
||||
}
|
||||
++p;
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
|
||||
std::vector<std::string> split(std::string str, char delim){
|
||||
std::istringstream iss(str);
|
||||
std::vector<std::string> tokens;
|
||||
std::string item;
|
||||
while(std::getline(iss, item, delim)) {
|
||||
tokens.push_back(item);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
std::string url_encode(const std::string &value) {
|
||||
std::ostringstream escaped;
|
||||
escaped.fill('0');
|
||||
escaped << std::hex;
|
||||
|
||||
for (std::string::const_iterator i = value.begin(), n = value.end(); i != n; ++i) {
|
||||
std::string::value_type c = (*i);
|
||||
|
||||
// Keep alphanumeric and other accepted characters intact
|
||||
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
escaped << c;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Any other characters are percent-encoded
|
||||
escaped << std::uppercase;
|
||||
escaped << '%' << std::setw(2) << int((unsigned char) c);
|
||||
escaped << std::nouppercase;
|
||||
}
|
||||
|
||||
return escaped.str();
|
||||
}
|
||||
|
||||
} // namespace ZeroTier
|
||||
|
||||
#endif
|
16
controller/CtlUtil.hpp
Normal file
16
controller/CtlUtil.hpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
#ifndef ZT_CTLUTIL_HPP
|
||||
#define ZT_CTLUTIL_HPP
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace ZeroTier {
|
||||
|
||||
const char *_timestr();
|
||||
|
||||
std::vector<std::string> split(std::string str, char delim);
|
||||
|
||||
std::string url_encode(const std::string &value);
|
||||
}
|
||||
|
||||
#endif // namespace ZeroTier
|
|
@ -75,6 +75,10 @@ public:
|
|||
*/
|
||||
class DB
|
||||
{
|
||||
#ifdef ZT_CONTROLLER_USE_LIBPQ
|
||||
friend class MemberNotificationReceiver;
|
||||
friend class NetworkNotificationReceiver;
|
||||
#endif
|
||||
public:
|
||||
class ChangeListener
|
||||
{
|
||||
|
|
|
@ -40,7 +40,8 @@
|
|||
#include "LFDB.hpp"
|
||||
#include "FileDB.hpp"
|
||||
#ifdef ZT_CONTROLLER_USE_LIBPQ
|
||||
#include "PostgreSQL.hpp"
|
||||
#include "CV1.hpp"
|
||||
#include "CV2.hpp"
|
||||
#endif
|
||||
|
||||
#include "../node/Node.hpp"
|
||||
|
@ -534,7 +535,9 @@ void EmbeddedNetworkController::init(const Identity &signingId,Sender *sender)
|
|||
|
||||
#ifdef ZT_CONTROLLER_USE_LIBPQ
|
||||
if ((_path.length() > 9)&&(_path.substr(0,9) == "postgres:")) {
|
||||
_db.addDB(std::shared_ptr<DB>(new PostgreSQL(_signingId,_path.substr(9).c_str(), _listenPort, _rc)));
|
||||
_db.addDB(std::shared_ptr<DB>(new CV1(_signingId,_path.substr(9).c_str(), _listenPort, _rc)));
|
||||
} else if ((_path.length() > 3)&&(_path.substr(0,3) == "cv2:")) {
|
||||
_db.addDB(std::shared_ptr<DB>(new CV2(_signingId,_path.c_str(),_listenPort)));
|
||||
} else {
|
||||
#endif
|
||||
_db.addDB(std::shared_ptr<DB>(new FileDB(_path.c_str())));
|
||||
|
@ -1548,7 +1551,8 @@ void EmbeddedNetworkController::_request(
|
|||
authInfo.add(ZT_AUTHINFO_DICT_KEY_CENTRAL_ENDPOINT_URL, info.centralAuthURL.c_str());
|
||||
authInfo.add(ZT_AUTHINFO_DICT_KEY_NONCE, info.ssoNonce.c_str());
|
||||
authInfo.add(ZT_AUTHINFO_DICT_KEY_STATE, info.ssoState.c_str());
|
||||
authInfo.add(ZT_AUTHINFO_DICT_KEY_CLIENT_ID, info.ssoClientID.c_str());
|
||||
authInfo.add(ZT_AUTHINFO_DICT_KEY_CLIENT_ID, info.ssoClientID.c_str());
|
||||
authInfo.add(ZT_AUTHINFO_DICT_KEY_SSO_PROVIDER, info.ssoProvider.c_str());
|
||||
_sender->ncSendError(nwid,requestPacketId,identity.address(),NetworkController::NC_ERROR_AUTHENTICATION_REQUIRED, authInfo.data(), authInfo.sizeBytes());
|
||||
}
|
||||
DB::cleanMember(member);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c)2019 ZeroTier, Inc.
|
||||
* Copyright (c)2025 ZeroTier, Inc.
|
||||
*
|
||||
* Use of this software is governed by the Business Source License included
|
||||
* in the LICENSE.TXT file in the project's root directory.
|
||||
|
@ -11,35 +11,22 @@
|
|||
*/
|
||||
/****/
|
||||
|
||||
#include "DB.hpp"
|
||||
|
||||
#ifdef ZT_CONTROLLER_USE_LIBPQ
|
||||
|
||||
#ifndef ZT_CONTROLLER_LIBPQ_HPP
|
||||
#define ZT_CONTROLLER_LIBPQ_HPP
|
||||
|
||||
#define ZT_CENTRAL_CONTROLLER_COMMIT_THREADS 4
|
||||
#ifndef ZT_CONTROLLER_POSTGRESQL_HPP
|
||||
#define ZT_CONTROLLER_POSTGRESQL_HPP
|
||||
|
||||
#include "DB.hpp"
|
||||
#include "ConnectionPool.hpp"
|
||||
#include <pqxx/pqxx>
|
||||
|
||||
#include <memory>
|
||||
#include <redis++/redis++.h>
|
||||
|
||||
#include "../node/Metrics.hpp"
|
||||
namespace ZeroTier {
|
||||
|
||||
extern "C" {
|
||||
typedef struct pg_conn PGconn;
|
||||
}
|
||||
|
||||
namespace smeeclient {
|
||||
struct SmeeClient;
|
||||
}
|
||||
|
||||
namespace ZeroTier {
|
||||
|
||||
struct RedisConfig;
|
||||
|
||||
|
||||
class PostgresConnection : public Connection {
|
||||
public:
|
||||
|
@ -68,129 +55,32 @@ private:
|
|||
std::string m_connString;
|
||||
};
|
||||
|
||||
class PostgreSQL;
|
||||
|
||||
class MemberNotificationReceiver : public pqxx::notification_receiver {
|
||||
public:
|
||||
MemberNotificationReceiver(PostgreSQL *p, pqxx::connection &c, const std::string &channel);
|
||||
MemberNotificationReceiver(DB *p, pqxx::connection &c, const std::string &channel);
|
||||
virtual ~MemberNotificationReceiver() {
|
||||
fprintf(stderr, "MemberNotificationReceiver destroyed\n");
|
||||
}
|
||||
|
||||
virtual void operator() (const std::string &payload, int backendPid);
|
||||
private:
|
||||
PostgreSQL *_psql;
|
||||
DB *_psql;
|
||||
};
|
||||
|
||||
class NetworkNotificationReceiver : public pqxx::notification_receiver {
|
||||
public:
|
||||
NetworkNotificationReceiver(PostgreSQL *p, pqxx::connection &c, const std::string &channel);
|
||||
NetworkNotificationReceiver(DB *p, pqxx::connection &c, const std::string &channel);
|
||||
virtual ~NetworkNotificationReceiver() {
|
||||
fprintf(stderr, "NetworkNotificationReceiver destroyed\n");
|
||||
};
|
||||
|
||||
virtual void operator() (const std::string &payload, int packend_pid);
|
||||
private:
|
||||
PostgreSQL *_psql;
|
||||
};
|
||||
|
||||
/**
|
||||
* A controller database driver that talks to PostgreSQL
|
||||
*
|
||||
* This is for use with ZeroTier Central. Others are free to build and use it
|
||||
* but be aware that we might change it at any time.
|
||||
*/
|
||||
class PostgreSQL : public DB
|
||||
{
|
||||
friend class MemberNotificationReceiver;
|
||||
friend class NetworkNotificationReceiver;
|
||||
public:
|
||||
PostgreSQL(const Identity &myId, const char *path, int listenPort, RedisConfig *rc);
|
||||
virtual ~PostgreSQL();
|
||||
|
||||
virtual bool waitForReady();
|
||||
virtual bool isReady();
|
||||
virtual bool save(nlohmann::json &record,bool notifyListeners);
|
||||
virtual void eraseNetwork(const uint64_t networkId);
|
||||
virtual void eraseMember(const uint64_t networkId, const uint64_t memberId);
|
||||
virtual void nodeIsOnline(const uint64_t networkId, const uint64_t memberId, const InetAddress &physicalAddress);
|
||||
virtual AuthInfo getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL);
|
||||
|
||||
protected:
|
||||
struct _PairHasher
|
||||
{
|
||||
inline std::size_t operator()(const std::pair<uint64_t,uint64_t> &p) const { return (std::size_t)(p.first ^ p.second); }
|
||||
};
|
||||
virtual void _memberChanged(nlohmann::json &old,nlohmann::json &memberConfig,bool notifyListeners) {
|
||||
DB::_memberChanged(old, memberConfig, notifyListeners);
|
||||
}
|
||||
|
||||
virtual void _networkChanged(nlohmann::json &old,nlohmann::json &networkConfig,bool notifyListeners) {
|
||||
DB::_networkChanged(old, networkConfig, notifyListeners);
|
||||
}
|
||||
|
||||
private:
|
||||
void initializeNetworks();
|
||||
void initializeMembers();
|
||||
void heartbeat();
|
||||
void membersDbWatcher();
|
||||
void _membersWatcher_Postgres();
|
||||
void networksDbWatcher();
|
||||
void _networksWatcher_Postgres();
|
||||
|
||||
void _membersWatcher_Redis();
|
||||
void _networksWatcher_Redis();
|
||||
|
||||
void commitThread();
|
||||
void onlineNotificationThread();
|
||||
void onlineNotification_Postgres();
|
||||
void onlineNotification_Redis();
|
||||
uint64_t _doRedisUpdate(sw::redis::Transaction &tx, std::string &controllerId,
|
||||
std::unordered_map< std::pair<uint64_t,uint64_t>,std::pair<int64_t,InetAddress>,_PairHasher > &lastOnline);
|
||||
|
||||
void configureSmee();
|
||||
void notifyNewMember(const std::string &networkID, const std::string &memberID);
|
||||
|
||||
enum OverrideMode {
|
||||
ALLOW_PGBOUNCER_OVERRIDE = 0,
|
||||
NO_OVERRIDE = 1
|
||||
};
|
||||
|
||||
std::shared_ptr<ConnectionPool<PostgresConnection> > _pool;
|
||||
|
||||
const Identity _myId;
|
||||
const Address _myAddress;
|
||||
std::string _myAddressStr;
|
||||
std::string _connString;
|
||||
|
||||
BlockingQueue< std::pair<nlohmann::json,bool> > _commitQueue;
|
||||
|
||||
std::thread _heartbeatThread;
|
||||
std::thread _membersDbWatcher;
|
||||
std::thread _networksDbWatcher;
|
||||
std::thread _commitThread[ZT_CENTRAL_CONTROLLER_COMMIT_THREADS];
|
||||
std::thread _onlineNotificationThread;
|
||||
|
||||
std::unordered_map< std::pair<uint64_t,uint64_t>,std::pair<int64_t,InetAddress>,_PairHasher > _lastOnline;
|
||||
|
||||
mutable std::mutex _lastOnline_l;
|
||||
mutable std::mutex _readyLock;
|
||||
std::atomic<int> _ready, _connected, _run;
|
||||
mutable volatile bool _waitNoticePrinted;
|
||||
|
||||
int _listenPort;
|
||||
uint8_t _ssoPsk[48];
|
||||
|
||||
RedisConfig *_rc;
|
||||
std::shared_ptr<sw::redis::Redis> _redis;
|
||||
std::shared_ptr<sw::redis::RedisCluster> _cluster;
|
||||
bool _redisMemberStatus;
|
||||
|
||||
smeeclient::SmeeClient *_smee;
|
||||
DB *_psql;
|
||||
};
|
||||
|
||||
} // namespace ZeroTier
|
||||
|
||||
#endif // ZT_CONTROLLER_LIBPQ_HPP
|
||||
#endif // ZT_CONTROLLER_POSTGRESQL_HPP
|
||||
|
||||
#endif // ZT_CONTROLLER_USE_LIBPQ
|
||||
#endif // ZT_CONTROLLER_USE_LIBPQ
|
6
debian/changelog
vendored
6
debian/changelog
vendored
|
@ -1,3 +1,9 @@
|
|||
zerotier-one (1.14.2) unstable; urgency=medium
|
||||
|
||||
* See RELEASE-NOTES.md for release notes.
|
||||
|
||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 23 Oct 2024 01:00:00 -0700
|
||||
|
||||
zerotier-one (1.14.1) unstable; urgency=medium
|
||||
|
||||
* See RELEASE-NOTES.md for release notes.
|
||||
|
|
|
@ -9,15 +9,16 @@ mkztfile() {
|
|||
file=$1
|
||||
mode=$2
|
||||
content=$3
|
||||
|
||||
echo "creating $file"
|
||||
mkdir -p /var/lib/zerotier-one
|
||||
echo "$content" > "/var/lib/zerotier-one/$file"
|
||||
echo -n "$content" > "/var/lib/zerotier-one/$file"
|
||||
chmod "$mode" "/var/lib/zerotier-one/$file"
|
||||
}
|
||||
|
||||
if [ "x$ZEROTIER_API_SECRET" != "x" ]
|
||||
then
|
||||
mkztfile authtoken.secret 0600 "$ZEROTIER_API_SECRET"
|
||||
mkztfile metricstoken.secret 0600 "$ZEROTIER_API_SECRET"
|
||||
fi
|
||||
|
||||
if [ "x$ZEROTIER_IDENTITY_PUBLIC" != "x" ]
|
||||
|
@ -30,6 +31,11 @@ then
|
|||
mkztfile identity.secret 0600 "$ZEROTIER_IDENTITY_SECRET"
|
||||
fi
|
||||
|
||||
if [ "x$ZEROTIER_LOCAL_CONF" != "x" ]
|
||||
then
|
||||
mkztfile local.conf 0644 "$ZEROTIER_LOCAL_CONF"
|
||||
fi
|
||||
|
||||
mkztfile zerotier-one.port 0600 "9993"
|
||||
|
||||
killzerotier() {
|
||||
|
|
|
@ -701,7 +701,7 @@
|
|||
<key>USE_HFS+_COMPRESSION</key>
|
||||
<false/>
|
||||
<key>VERSION</key>
|
||||
<string>1.14.1</string>
|
||||
<string>1.14.2</string>
|
||||
</dict>
|
||||
<key>TYPE</key>
|
||||
<integer>0</integer>
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
<ROW Property="AiFeatIcoZeroTierOne" Value="ZeroTierIcon.exe" Type="8"/>
|
||||
<ROW Property="MSIFASTINSTALL" MultiBuildValue="DefaultBuild:2"/>
|
||||
<ROW Property="Manufacturer" Value="ZeroTier, Inc."/>
|
||||
<ROW Property="ProductCode" Value="1033:{EC58088A-4E0F-4BD5-B0B2-FD81C803EEC4} " Type="16"/>
|
||||
<ROW Property="ProductCode" Value="1033:{0143A36C-46C6-458D-AB9B-C8843E089323} " Type="16"/>
|
||||
<ROW Property="ProductLanguage" Value="1033"/>
|
||||
<ROW Property="ProductName" Value="ZeroTier One"/>
|
||||
<ROW Property="ProductVersion" Value="1.14.0" Options="32"/>
|
||||
<ROW Property="ProductVersion" Value="1.14.2" Options="32"/>
|
||||
<ROW Property="REBOOT" MultiBuildValue="DefaultBuild:ReallySuppress"/>
|
||||
<ROW Property="SecureCustomProperties" Value="OLDPRODUCTS;AI_NEWERPRODUCTFOUND;AI_SETUPEXEPATH;SETUPEXEDIR"/>
|
||||
<ROW Property="UpgradeCode" Value="{B0E2A5F3-88B6-4E77-B922-CB4739B4C4C8}"/>
|
||||
|
@ -62,7 +62,7 @@
|
|||
<ROW Directory="regid.201001.com.zerotier_Dir" Directory_Parent="CommonAppDataFolder" DefaultDir="REGID2~1.ZER|regid.2010-01.com.zerotier" DirectoryOptions="12"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.MsiCompsComponent">
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{8BC01817-02AC-4C44-A84C-0727BC5B6E22}" Directory_="APPDIR" Attributes="4" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{DFE7A60C-C2B9-41F6-9171-8955BA30E556}" Directory_="APPDIR" Attributes="4" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_DisableModify" ComponentId="{46FFA8C5-A0CB-4E05-9AD3-911D543DE8CA}" Directory_="APPDIR" Attributes="4" KeyPath="NoModify" Options="1"/>
|
||||
<ROW Component="AI_ExePath" ComponentId="{8E02B36C-7A19-429B-A93E-77A9261AC918}" Directory_="APPDIR" Attributes="4" KeyPath="AI_ExePath"/>
|
||||
<ROW Component="APPDIR" ComponentId="{4DD7907D-D7FE-4CD6-B1A0-B5C1625F5133}" Directory_="APPDIR" Attributes="0"/>
|
||||
|
@ -498,7 +498,7 @@
|
|||
<ROW XmlAttribute="xsischemaLocation" XmlElement="swidsoftware_identification_tag" Name="xsi:schemaLocation" Flags="14" Order="3" Value="http://standards.iso.org/iso/19770/-2/2008/schema.xsd software_identification_tag.xsd"/>
|
||||
</COMPONENT>
|
||||
<COMPONENT cid="caphyon.advinst.msicomp.XmlElementComponent">
|
||||
<ROW XmlElement="swidbuild" ParentElement="swidnumeric" Name="swid:build" Condition="1" Order="2" Flags="14" Text="0" UpdateIndexInParent="0"/>
|
||||
<ROW XmlElement="swidbuild" ParentElement="swidnumeric" Name="swid:build" Condition="1" Order="2" Flags="14" Text="2" UpdateIndexInParent="0"/>
|
||||
<ROW XmlElement="swidentitlement_required_indicator" ParentElement="swidsoftware_identification_tag" Name="swid:entitlement_required_indicator" Condition="1" Order="0" Flags="14" Text="false" UpdateIndexInParent="0"/>
|
||||
<ROW XmlElement="swidmajor" ParentElement="swidnumeric" Name="swid:major" Condition="1" Order="0" Flags="14" Text="1" UpdateIndexInParent="0"/>
|
||||
<ROW XmlElement="swidminor" ParentElement="swidnumeric" Name="swid:minor" Condition="1" Order="1" Flags="14" Text="14" UpdateIndexInParent="0"/>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
namespace prometheus {
|
||||
|
||||
// структура, в которую копируются значения метрик перед их сериализацией
|
||||
|
||||
struct ClientMetric {
|
||||
|
||||
// Label
|
||||
|
|
|
@ -84,7 +84,7 @@ extern "C" {
|
|||
/**
|
||||
* Minimum UDP payload size allowed
|
||||
*/
|
||||
#define ZT_MIN_PHYSMTU 1400
|
||||
#define ZT_MIN_PHYSMTU 510
|
||||
|
||||
/**
|
||||
* Maximum physical interface name length. This number is gigantic because of Windows.
|
||||
|
|
20
make-mac.mk
20
make-mac.mk
|
@ -57,9 +57,9 @@ ONE_OBJS+=ext/libnatpmp/natpmp.o ext/libnatpmp/getgateway.o ext/miniupnpc/connec
|
|||
ifeq ($(ZT_CONTROLLER),1)
|
||||
MACOS_VERSION_MIN=10.15
|
||||
override CXXFLAGS=$(CFLAGS) -std=c++17 -stdlib=libc++
|
||||
LIBS+=-L/usr/local/opt/libpqxx/lib -L/usr/local/opt/libpq/lib -L/usr/local/opt/openssl/lib/ -lpqxx -lpq -lssl -lcrypto -lgssapi_krb5 ext/redis-plus-plus-1.1.1/install/macos/lib/libredis++.a ext/hiredis-0.14.1/lib/macos/libhiredis.a
|
||||
LIBS+=-L/opt/homebrew/lib -L/usr/local/opt/libpqxx/lib -L/usr/local/opt/libpq/lib -L/usr/local/opt/openssl/lib/ -lpqxx -lpq -lssl -lcrypto -lgssapi_krb5 ext/redis-plus-plus-1.1.1/install/macos/lib/libredis++.a ext/hiredis-0.14.1/lib/macos/libhiredis.a rustybits/target/libsmeeclient.a
|
||||
DEFS+=-DZT_CONTROLLER_USE_LIBPQ -DZT_CONTROLLER_USE_REDIS -DZT_CONTROLLER
|
||||
INCLUDES+=-I/usr/local/opt/libpq/include -I/usr/local/opt/libpqxx/include -Iext/hiredis-0.14.1/include/ -Iext/redis-plus-plus-1.1.1/install/macos/include/sw/
|
||||
INCLUDES+=-I/opt/homebrew/include -I/opt/homebrew/opt/libpq/include -I/usr/local/opt/libpq/include -I/usr/local/opt/libpqxx/include -Iext/hiredis-0.14.1/include/ -Iext/redis-plus-plus-1.1.1/install/macos/include/sw/ -Irustybits/target/
|
||||
else
|
||||
MACOS_VERSION_MIN=10.13
|
||||
endif
|
||||
|
@ -115,7 +115,11 @@ mac-agent: FORCE
|
|||
osdep/MacDNSHelper.o: osdep/MacDNSHelper.mm
|
||||
$(CXX) $(CXXFLAGS) -c osdep/MacDNSHelper.mm -o osdep/MacDNSHelper.o
|
||||
|
||||
ifeq ($(ZT_CONTROLLER),1)
|
||||
one: zeroidc smeeclient $(CORE_OBJS) $(ONE_OBJS) one.o mac-agent
|
||||
else
|
||||
one: zeroidc $(CORE_OBJS) $(ONE_OBJS) one.o mac-agent
|
||||
endif
|
||||
$(CXX) $(CXXFLAGS) -o zerotier-one $(CORE_OBJS) $(ONE_OBJS) one.o $(LIBS) rustybits/target/libzeroidc.a
|
||||
# $(STRIP) zerotier-one
|
||||
ln -sf zerotier-one zerotier-idtool
|
||||
|
@ -126,6 +130,15 @@ zerotier-one: one
|
|||
|
||||
zeroidc: rustybits/target/libzeroidc.a
|
||||
|
||||
ifeq ($(ZT_CONTROLLER),1)
|
||||
smeeclient: rustybits/target/libsmeeclient.a
|
||||
|
||||
rustybits/target/libsmeeclient.a: FORCE
|
||||
cd rustybits && MACOSX_DEPLOYMENT_TARGET=$(MACOS_VERSION_MIN) cargo build -p smeeclient --target=x86_64-apple-darwin $(EXTRA_CARGO_FLAGS)
|
||||
cd rustybits && MACOSX_DEPLOYMENT_TARGET=$(MACOS_VERSION_MIN) cargo build -p smeeclient --target=aarch64-apple-darwin $(EXTRA_CARGO_FLAGS)
|
||||
cd rustybits && lipo -create target/x86_64-apple-darwin/$(RUST_VARIANT)/libsmeeclient.a target/aarch64-apple-darwin/$(RUST_VARIANT)/libsmeeclient.a -output target/libsmeeclient.a
|
||||
endif
|
||||
|
||||
rustybits/target/libzeroidc.a: FORCE
|
||||
cd rustybits && MACOSX_DEPLOYMENT_TARGET=$(MACOS_VERSION_MIN) cargo build -p zeroidc --target=x86_64-apple-darwin $(EXTRA_CARGO_FLAGS)
|
||||
cd rustybits && MACOSX_DEPLOYMENT_TARGET=$(MACOS_VERSION_MIN) cargo build -p zeroidc --target=aarch64-apple-darwin $(EXTRA_CARGO_FLAGS)
|
||||
|
@ -194,6 +207,9 @@ controller-run: _buildx FORCE
|
|||
central-controller-docker: _buildx FORCE
|
||||
docker buildx build --platform linux/arm64,linux/amd64 --no-cache -t registry.zerotier.com/zerotier-central/ztcentral-controller:${TIMESTAMP} -f ext/central-controller-docker/Dockerfile --build-arg git_branch=$(shell git name-rev --name-only HEAD) . --push
|
||||
@echo Image: registry.zerotier.com/zerotier-central/ztcentral-controller:${TIMESTAMP}
|
||||
|
||||
docker-release: _buildx
|
||||
docker buildx build --platform linux/386,linux/amd64,linux/arm/v7,linux/arm64,linux/mips64le,linux/ppc64le,linux/s390x -t zerotier/zerotier:${RELEASE_DOCKER_TAG} -t zerotier/zerotier:latest --build-arg VERSION=${RELEASE_VERSION} -f Dockerfile.release . --push
|
||||
|
||||
clean:
|
||||
rm -rf MacEthernetTapAgent *.dSYM build-* *.a *.pkg *.dmg *.o node/*.o controller/*.o service/*.o osdep/*.o ext/http-parser/*.o $(CORE_OBJS) $(ONE_OBJS) zerotier-one zerotier-idtool zerotier-selftest zerotier-cli zerotier doc/node_modules zt1_update_$(ZT_BUILD_PLATFORM)_$(ZT_BUILD_ARCHITECTURE)_* rustybits/target/
|
||||
|
|
|
@ -30,7 +30,8 @@ CORE_OBJS=\
|
|||
node/Trace.o \
|
||||
node/Utils.o \
|
||||
node/Bond.o \
|
||||
node/PacketMultiplexer.o
|
||||
node/PacketMultiplexer.o \
|
||||
osdep/OSUtils.o
|
||||
|
||||
ONE_OBJS=\
|
||||
controller/EmbeddedNetworkController.o \
|
||||
|
@ -38,11 +39,13 @@ ONE_OBJS=\
|
|||
controller/DB.o \
|
||||
controller/FileDB.o \
|
||||
controller/LFDB.o \
|
||||
controller/CtlUtil.o \
|
||||
controller/PostgreSQL.o \
|
||||
controller/CV1.o \
|
||||
controller/CV2.o \
|
||||
osdep/EthernetTap.o \
|
||||
osdep/ManagedRoute.o \
|
||||
osdep/Http.o \
|
||||
osdep/OSUtils.o \
|
||||
service/SoftwareUpdater.o \
|
||||
service/OneService.o
|
||||
|
||||
|
|
|
@ -431,10 +431,13 @@ void BSDEthernetTap::threadMain()
|
|||
// constructing itself.
|
||||
Thread::sleep(500);
|
||||
|
||||
for (unsigned int i = 0; i < _concurrency; ++i) {
|
||||
_rxThreads.push_back(std::thread([this, i, _pinning] {
|
||||
#ifndef __OpenBSD__
|
||||
bool pinning = _pinning;
|
||||
|
||||
if (_pinning) {
|
||||
for (unsigned int i = 0; i < _concurrency; ++i) {
|
||||
_rxThreads.push_back(std::thread([this, i, pinning] {
|
||||
|
||||
if (pinning) {
|
||||
int pinCore = i % _concurrency;
|
||||
fprintf(stderr, "Pinning thread %d to core %d\n", i, pinCore);
|
||||
pthread_t self = pthread_self();
|
||||
|
@ -449,6 +452,7 @@ void BSDEthernetTap::threadMain()
|
|||
exit(1);
|
||||
}
|
||||
}
|
||||
#endif // __OpenBSD__
|
||||
|
||||
uint8_t b[ZT_TAP_BUF_SIZE];
|
||||
MAC to, from;
|
||||
|
@ -495,8 +499,10 @@ void BSDEthernetTap::threadMain()
|
|||
}
|
||||
}
|
||||
}
|
||||
#ifndef __OpenBSD__
|
||||
}));
|
||||
}
|
||||
#endif // __OpenBSD__
|
||||
}
|
||||
|
||||
} // namespace ZeroTier
|
||||
|
|
|
@ -140,7 +140,7 @@ std::shared_ptr<EthernetTap> EthernetTap::newInstance(
|
|||
#endif // __NetBSD__
|
||||
|
||||
#ifdef __OpenBSD__
|
||||
return std::shared_ptr<EthernetTap>(new BSDEthernetTap(homePath,mac,mtu,metric,nwid,friendlyName,handler,arg));
|
||||
return std::shared_ptr<EthernetTap>(new BSDEthernetTap(homePath,concurrency,pinning,mac,mtu,metric,nwid,friendlyName,handler,arg));
|
||||
#endif // __OpenBSD__
|
||||
|
||||
#endif // ZT_SDK?
|
||||
|
|
1043
rustybits/Cargo.lock
generated
1043
rustybits/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -16,7 +16,10 @@ use serde::{Deserialize, Serialize};
|
|||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use temporal_client::{Client, ClientOptionsBuilder, RetryClient, WorkflowClientTrait, WorkflowOptions};
|
||||
use temporal_sdk_core_protos::{coresdk::AsJsonPayloadExt, temporal::api::enums::v1::WorkflowIdReusePolicy};
|
||||
use temporal_sdk_core_protos::{
|
||||
coresdk::AsJsonPayloadExt,
|
||||
temporal::api::enums::v1::{WorkflowIdConflictPolicy, WorkflowIdReusePolicy},
|
||||
};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -72,6 +75,7 @@ impl SmeeClient {
|
|||
println!("notifying network joined");
|
||||
let options = WorkflowOptions {
|
||||
id_reuse_policy: WorkflowIdReusePolicy::RejectDuplicate,
|
||||
id_conflict_policy: WorkflowIdConflictPolicy::Fail,
|
||||
execution_timeout: None,
|
||||
run_timeout: None,
|
||||
task_timeout: None,
|
||||
|
|
|
@ -2599,6 +2599,7 @@ public:
|
|||
fprintf(stderr,"WARNING: using manually-specified secondary and/or tertiary ports. This can cause NAT issues." ZT_EOL_S);
|
||||
}
|
||||
_portMappingEnabled = OSUtils::jsonBool(settings["portMappingEnabled"],true);
|
||||
_node->setLowBandwidthMode(OSUtils::jsonBool(settings["lowBandwidthMode"],false));
|
||||
#if defined(__LINUX__) || defined(__FreeBSD__)
|
||||
_multicoreEnabled = OSUtils::jsonBool(settings["multicoreEnabled"],false);
|
||||
_concurrency = OSUtils::jsonInt(settings["concurrency"],1);
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
/**
|
||||
* Revision
|
||||
*/
|
||||
#define ZEROTIER_ONE_VERSION_REVISION 1
|
||||
#define ZEROTIER_ONE_VERSION_REVISION 2
|
||||
|
||||
/**
|
||||
* Build version
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Name: zerotier-one
|
||||
Version: 1.14.1
|
||||
Version: 1.14.2
|
||||
Release: 1%{?dist}
|
||||
Summary: ZeroTier network virtualization service
|
||||
|
||||
|
@ -155,6 +155,9 @@ chmod 0755 $RPM_BUILD_ROOT/etc/init.d/zerotier-one
|
|||
%endif
|
||||
|
||||
%changelog
|
||||
* Wed Oct 23 2024 Adam Ierymenko <adam.ierymenko@zerotier.com> - 1.14.2
|
||||
- see https://github.com/zerotier/ZeroTierOne for release notes
|
||||
|
||||
* Tue Mar 19 2024 Adam Ierymenko <adam.ierymenko@zerotier.com> - 1.14.0
|
||||
- see https://github.com/zerotier/ZeroTierOne for release notes
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue