1
#include "cilium/host_map.h"
2

            
3
#include <arpa/inet.h>
4
#include <fmt/format.h>
5
#include <sys/socket.h>
6

            
7
#include <cstdint>
8
#include <cstring>
9
#include <memory>
10
#include <string>
11
#include <utility>
12
#include <vector>
13

            
14
#include "envoy/common/exception.h"
15
#include "envoy/config/subscription.h"
16
#include "envoy/event/dispatcher.h"
17
#include "envoy/server/factory_context.h"
18
#include "envoy/thread_local/thread_local.h"
19
#include "envoy/thread_local/thread_local_object.h"
20

            
21
#include "source/common/common/logger.h"
22
#include "source/common/common/macros.h"
23

            
24
#include "absl/numeric/int128.h"
25
#include "absl/status/status.h"
26
#include "absl/strings/string_view.h"
27
#include "cilium/api/nphds.pb.h"
28
#include "cilium/grpc_subscription.h"
29

            
30
namespace Envoy {
31
namespace Cilium {
32

            
33
template <typename T>
34
371
unsigned int checkPrefix(T addr, bool have_prefix, unsigned int plen, absl::string_view host) {
35
371
  const unsigned int plen_max = sizeof(T) * 8;
36
371
  if (!have_prefix) {
37
196
    return plen_max;
38
196
  }
39
175
  if (plen > plen_max) {
40
1
    throw EnvoyException(fmt::format("NetworkPolicyHosts: Invalid prefix length in \'{}\'", host));
41
1
  }
42
  // Check for 1-bits after the prefix
43
174
  if ((plen == 0 && addr) || (plen > 0 && addr & ntoh((T(1) << (plen_max - plen)) - 1))) {
44
2
    throw EnvoyException(fmt::format("NetworkPolicyHosts: Non-prefix bits set in \'{}\'", host));
45
2
  }
46
172
  return plen;
47
174
}
48

            
49
struct ThreadLocalHostMapInitializer : public PolicyHostMap::ThreadLocalHostMap {
50
protected:
51
  friend class PolicyHostMap; // PolicyHostMap can insert();
52

            
53
  // find the map of the given prefix length, insert in the decreasing order if
54
  // it does not exist
55
  template <typename M>
56
368
  M& getMap(std::vector<std::pair<unsigned int, M>>& maps, unsigned int plen) {
57
368
    auto it = maps.begin();
58
527
    for (; it != maps.end(); it++) {
59
264
      if (it->first > plen) {
60
159
        ENVOY_LOG(trace, "Skipping map for prefix length {} while looking for {}", it->first, plen);
61
159
        continue; // check the next one
62
159
      }
63
105
      if (it->first == plen) {
64
85
        ENVOY_LOG(trace, "Found existing map for prefix length {}", plen);
65
85
        return it->second;
66
85
      }
67
      // Current pair has smaller prefix, insert before it to maintain order
68
20
      ENVOY_LOG(trace, "Inserting map for prefix length {} before prefix length {}", plen,
69
20
                it->first);
70
20
      break;
71
105
    }
72
    // not found, insert before the position 'it'
73
283
    ENVOY_LOG(trace, "Inserting map for prefix length {}", plen);
74
283
    return maps.emplace(it, std::make_pair(plen, M{}))->second;
75
368
  }
76

            
77
202
  bool insert(uint32_t addr, unsigned int plen, uint64_t policy) {
78
202
    auto pair = getMap(ipv4_to_policy_, plen).emplace(std::make_pair(addr, policy));
79
202
    return pair.second;
80
202
  }
81

            
82
166
  bool insert(absl::uint128 addr, unsigned int plen, uint64_t policy) {
83
166
    auto pair = getMap(ipv6_to_policy_, plen).emplace(std::make_pair(addr, policy));
84
166
    return pair.second;
85
166
  }
86

            
87
193
  void insert(const cilium::NetworkPolicyHosts& proto) {
88
193
    uint64_t policy = proto.policy();
89
193
    const auto& hosts = proto.host_addresses();
90
193
    std::string buf;
91

            
92
375
    for (const auto& host : hosts) {
93
375
      const char* addr = host.c_str();
94
375
      unsigned int plen = 0;
95

            
96
375
      ENVOY_LOG(trace, "NetworkPolicyHosts: Inserting CIDR->ID mapping {}->{}...", host, policy);
97

            
98
      // Find the prefix length if any
99
375
      const char* slash = strchr(addr, '/');
100
375
      bool have_prefix = (slash != nullptr);
101
375
      if (have_prefix) {
102
177
        const char* pstr = slash + 1;
103
        // Must start with a digit and have nothing after a zero.
104
177
        if (*pstr < '0' || *pstr > '9' || (*pstr == '0' && *(pstr + 1) != '\0')) {
105
1
          throw EnvoyException(
106
1
              fmt::format("NetworkPolicyHosts: Invalid prefix length in \'{}\'", host));
107
1
        }
108
        // Convert to base 10 integer as long as there are digits and plen is
109
        // not too large. If plen is already 13, next digit will make it at
110
        // least 130, which is too much.
111
508
        while (*pstr >= '0' && *pstr <= '9' && plen < 13) {
112
332
          plen = plen * 10 + (*pstr++ - '0');
113
332
        }
114
176
        if (*pstr != '\0') {
115
1
          throw EnvoyException(
116
1
              fmt::format("NetworkPolicyHosts: Invalid prefix length in \'{}\'", host));
117
1
        }
118
        // Copy the address without the prefix
119
175
        buf.assign(addr, slash);
120
175
        addr = buf.c_str();
121
175
      }
122

            
123
373
      uint32_t addr4;
124
373
      int rc = inet_pton(AF_INET, addr, &addr4);
125
373
      if (rc == 1) {
126
205
        plen = checkPrefix(addr4, have_prefix, plen, host);
127
205
        if (!insert(addr4, plen, policy)) {
128
2
          uint64_t existing_policy = resolve(addr4);
129
2
          throw EnvoyException(fmt::format("NetworkPolicyHosts: Duplicate host entry \'{}\' for "
130
2
                                           "policy {}, already mapped to {}",
131
2
                                           host, policy, existing_policy));
132
2
        }
133
203
        continue;
134
205
      }
135
168
      absl::uint128 addr6;
136
168
      rc = inet_pton(AF_INET6, addr, &addr6);
137
168
      if (rc == 1) {
138
166
        plen = checkPrefix(addr6, have_prefix, plen, host);
139
166
        if (!insert(addr6, plen, policy)) {
140
          uint64_t existing_policy = resolve(addr6);
141
          throw EnvoyException(fmt::format("NetworkPolicyHosts: Duplicate host entry \'{}\' for "
142
                                           "policy {}, already mapped to {}",
143
                                           host, policy, existing_policy));
144
        }
145
166
        continue;
146
166
      }
147
2
      throw EnvoyException(
148
2
          fmt::format("NetworkPolicyHosts: Invalid host entry \'{}\' for policy {}", host, policy));
149
168
    }
150
193
  }
151
};
152

            
153
uint64_t PolicyHostMap::instance_id_ = 0;
154

            
155
// This is used directly for testing with a file-based subscription
156
139
PolicyHostMap::PolicyHostMap(ThreadLocal::SlotAllocator& tls) : tls_(tls.allocateSlot()) {
157
139
  instance_id_++;
158
139
  name_ = "cilium.hostmap." + fmt::format("{}", instance_id_) + ".";
159
139
  ENVOY_LOG(debug, "PolicyHostMap({}) created.", name_);
160

            
161
139
  auto empty_map = std::make_shared<ThreadLocalHostMapInitializer>();
162
252
  tls_->set([empty_map](Event::Dispatcher&) -> ThreadLocal::ThreadLocalObjectSharedPtr {
163
252
    return empty_map;
164
252
  });
165
139
}
166

            
167
// This is used in production
168
PolicyHostMap::PolicyHostMap(Server::Configuration::CommonFactoryContext& context)
169
129
    : PolicyHostMap(context.threadLocal()) {
170
129
  scope_ = context.serverScope().createScope(name_);
171
129
}
172

            
173
void PolicyHostMap::startSubscription(Server::Configuration::CommonFactoryContext& context) {
174
  subscription_ = subscribe("type.googleapis.com/cilium.NetworkPolicyHosts", context.localInfo(),
175
                            context.clusterManager(), context.mainThreadDispatcher(),
176
                            context.api().randomGenerator(), *scope_, *this,
177
                            std::make_shared<Cilium::PolicyHostDecoder>());
178
  subscription_->start({});
179
}
180

            
181
absl::Status
182
PolicyHostMap::onConfigUpdate(const std::vector<Envoy::Config::DecodedResourceRef>& resources,
183
139
                              const std::string& version_info) {
184
139
  ENVOY_LOG(debug, "PolicyHostMap::onConfigUpdate({}), {} resources, version: {}", name_,
185
139
            resources.size(), version_info);
186

            
187
139
  auto newmap = std::make_shared<ThreadLocalHostMapInitializer>();
188

            
189
241
  for (const auto& resource : resources) {
190
193
    const auto& config = dynamic_cast<const cilium::NetworkPolicyHosts&>(resource.get().resource());
191
193
    ENVOY_LOG(trace,
192
193
              "Received NetworkPolicyHosts for policy {} in onConfigUpdate() "
193
193
              "version {}",
194
193
              config.policy(), version_info);
195
193
    newmap->insert(config);
196
193
  }
197

            
198
  // Force 'this' to be not deleted for as long as the lambda stays
199
  // alive. Note that generally capturing a shared pointer is
200
  // dangerous as it may happen that there is a circular reference
201
  // from 'this' to itself via the lambda capture, leading to 'this'
202
  // never being released. It should happen in this case, though.
203
139
  std::shared_ptr<PolicyHostMap> shared_this = shared_from_this();
204

            
205
  // Assign the new map to all threads.
206
243
  tls_->set([shared_this, newmap](Event::Dispatcher&) -> ThreadLocal::ThreadLocalObjectSharedPtr {
207
243
    UNREFERENCED_PARAMETER(shared_this);
208
243
    ENVOY_LOG(trace, "PolicyHostMap: Assigning new map");
209
243
    return newmap;
210
243
  });
211
139
  logmaps("onConfigUpdate");
212
139
  return absl::OkStatus();
213
139
}
214

            
215
void PolicyHostMap::onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason,
216
                                         const EnvoyException*) {
217
  // We need to allow server startup to continue, even if we have a bad
218
  // config.
219
}
220

            
221
} // namespace Cilium
222
} // namespace Envoy