Repository navigation
Expand file tree
/
Copy pathrotate_ladder.py
More file actions
375 lines (298 loc) · 13.7 KB
/
Copy pathrotate_ladder.py
File metadata and controls
375 lines (298 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
import boto3
import subprocess
import os
import requests
import json
import time
from datetime import datetime, timezone
# AWS region where EC2 instance is located
AWS_REGION = os.environ.get('NINJADOH_AWS_REGION', 'us-east-2')
# EC2 Instance ID to which the EIPs are associated (set NINJADOH_EC2_INSTANCE_ID when running)
INSTANCE_ID = os.environ.get('NINJADOH_EC2_INSTANCE_ID')
# Directory where certificates are stored
CERT_DIR = os.environ.get('NINJADOH_CERT_DIR', '/home/ubuntu/adguard/certs/')
# File to store the last used interface
LAST_USED_INTERFACE_FILE = os.environ.get('NINJADOH_LAST_INTERFACE_FILE', '/home/ubuntu/last_used_interface.txt')
# IPFS API URL, assumes local node
IPFS_API_URL = os.environ.get('NINJADOH_IPFS_API_URL', 'http://127.0.0.1:5001/api/v0')
# IPNS key ID for pubsub (set NINJADOH_IPNS_NAME; generate with: ipfs key gen ninjadoh)
IPNS_NAME = os.environ.get('NINJADOH_IPNS_NAME')
def get_last_used_interface():
"""
Get the last used interface from a file.
"""
if os.path.exists(LAST_USED_INTERFACE_FILE):
with open(LAST_USED_INTERFACE_FILE, 'r') as file:
return file.read().strip()
return None
def save_last_used_interface(interface_description):
"""
Save the last used interface to a file.
"""
with open(LAST_USED_INTERFACE_FILE, 'w') as file:
file.write(interface_description)
def get_interfaces_by_description(instance_id):
"""
Get network interfaces with descriptions 'ladder2' (eth1) and 'ladder3' (eth2) for rotation.
"""
ec2_client = boto3.client('ec2', region_name=AWS_REGION)
response = ec2_client.describe_instances(InstanceIds=[instance_id])
ladder_interfaces = []
for reservation in response['Reservations']:
for instance in reservation['Instances']:
for iface in instance.get('NetworkInterfaces', []):
description = iface.get('Description', '')
if description in ['ladder2', 'ladder3']:
ladder_interfaces.append({
'Description': description,
'NetworkInterfaceId': iface['NetworkInterfaceId'],
'PublicIp': iface.get('Association', {}).get('PublicIp'),
'AllocationId': iface.get('Association', {}).get('AllocationId')
})
return ladder_interfaces
def rotate_between_ladder2_and_ladder3(instance_id):
"""
Rotate the EIP between 'ladder2' (eth1) and 'ladder3' (eth2).
"""
interfaces = get_interfaces_by_description(instance_id)
if len(interfaces) != 2:
print("Error: Could not find both 'ladder2' and 'ladder3' interfaces.")
return None
# Get last used interface
last_used = get_last_used_interface()
# Alternate between ladder2 and ladder3
if last_used == 'ladder2':
next_interface = 'ladder3'
else:
next_interface = 'ladder2'
# Find the next interface to rotate
next_interface_data = next(filter(lambda i: i['Description'] == next_interface, interfaces), None)
if next_interface_data:
print(f"Rotating EIP for {next_interface_data['Description']} (interface: {next_interface_data['NetworkInterfaceId']})")
# Allocate a new EIP and associate it with the chosen network interface
new_ip, new_allocation_id = allocate_and_associate_eip(next_interface_data['NetworkInterfaceId'])
# Adding sleep to give time for IP reassociation to stabilize
time.sleep(10)
# Dissociate and release the old EIP from the chosen interface
if next_interface_data['AllocationId']:
dissociate_and_release_old_eip(next_interface_data['AllocationId'], next_interface_data['NetworkInterfaceId'])
# Save this interface as the last used one for the next rotation
save_last_used_interface(next_interface)
return new_ip
else:
print(f"Error: Could not find the interface for {next_interface}")
return None
def get_all_eips(instance_id):
"""
Get all public EIPs currently associated with the instance.
"""
ec2_client = boto3.client('ec2', region_name=AWS_REGION)
response = ec2_client.describe_instances(InstanceIds=[instance_id])
eips = []
for reservation in response['Reservations']:
for instance in reservation['Instances']:
for iface in instance.get('NetworkInterfaces', []):
public_ip = iface.get('Association', {}).get('PublicIp')
if public_ip:
eips.append(public_ip)
return eips
def allocate_and_associate_eip(network_interface_id):
"""
Allocate a new EIP and associate it with the specified network interface.
"""
ec2_client = boto3.client('ec2', region_name=AWS_REGION)
# Allocate a new Elastic IP
response = ec2_client.allocate_address(Domain='vpc')
eip_allocation_id = response['AllocationId']
eip_public_ip = response['PublicIp']
print(f"Allocated new EIP: {eip_public_ip}")
# Associate the Elastic IP with the specified network interface
ec2_client.associate_address(NetworkInterfaceId=network_interface_id, AllocationId=eip_allocation_id)
print(f"Associated EIP {eip_public_ip} with network interface {network_interface_id}")
return eip_public_ip, eip_allocation_id
def dissociate_and_release_old_eip(allocation_id, network_interface_id):
"""
Dissociate and release the old EIP to prevent unnecessary charges.
"""
ec2_client = boto3.client('ec2', region_name=AWS_REGION)
# Dissociate the Elastic IP
ec2_client.disassociate_address(NetworkInterfaceId=network_interface_id)
print(f"Dissociated EIP with Allocation ID: {allocation_id} from interface {network_interface_id}")
# Release the Elastic IP
ec2_client.release_address(AllocationId=allocation_id)
print(f"Released old EIP with Allocation ID: {allocation_id}")
def generate_combined_certificates(ip_addresses):
"""
Generate server certificates using the list of IP addresses.
"""
os.chdir(CERT_DIR)
# Generate server private key
subprocess.run(['openssl', 'genrsa', '-out', 'adguard_combined.key', '2048'], check=True)
# Create cert signing request (CSR) with the first IP as Common Name
subprocess.run([
'openssl', 'req', '-new', '-key', 'adguard_combined.key', '-out', 'adguard_combined.csr',
'-subj', f'/CN={ip_addresses[0]}/O=secretlab/L=Norman/ST=OK/C=US'
], check=True)
# Create the .ext file for certificate, with multiple IP addresses under subjectAltName
with open('adguard_combined.ext', 'w') as ext_file:
ext_file.write("authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\n"
"keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment\n"
"subjectAltName = @alt_names\n\n[alt_names]\n")
for idx, ip in enumerate(ip_addresses, 1):
ext_file.write(f"IP.{idx} = {ip}\n")
# Sign the CSR to make the server cert
subprocess.run([
'openssl', 'x509', '-req', '-in', 'adguard_combined.csr', '-CA', 'myCA.pem', '-CAkey', 'myCA_no_pass.key',
'-CAcreateserial', '-out', 'adguard_combined.crt', '-days', '825', '-sha256', '-extfile', 'adguard_combined.ext'
], check=True)
print(f"Generated combined certificate for IPs: {', '.join(ip_addresses)}")
# Resart nginx service to take new certs
#subprocess.run([
# 'sudo', 'systemctl', 'restart', 'nginx'
#], check=True)
# Reload nginx service to take new certs
subprocess.run([
'nginx', '-s', 'reload'
], check=True)
### IPNS
def post_ip_to_ipfs(ip_address, query_path):
# Get the current timestamp with timezone awareness
timestamp = datetime.now(timezone.utc).isoformat() # UTC timestamp in ISO 8601 format
# Create content for IPFS including the IP address and timestamp
content = {
'ip': ip_address,
'query_path': query_path,
'timestamp': timestamp
}
content_json = json.dumps(content)
# Add content to IPFS
response = requests.post(f"{IPFS_API_URL}/add", files={'file': ('content.json', content_json)})
response.raise_for_status() # Raise an exception for HTTP errors
cid = response.json()['Hash']
print(f"Added content to IPFS with CID: {cid}")
# Pin the content to your local IPFS node
pin_response = requests.post(f"{IPFS_API_URL}/pin/add?arg={cid}")
pin_response.raise_for_status()
print(f"Pinned CID: {cid}")
# Add the content to MFS so it shows up in the Files section
add_to_mfs(cid, "/posted_ip.json") # You can specify the path inside MFS
return cid
def add_to_mfs(cid, mfs_path):
# Remove the file from MFS if it already exists
try:
rm_response = requests.post(f"{IPFS_API_URL}/files/rm?arg={mfs_path}&force=true")
if rm_response.status_code == 200:
print(f"Removed existing file at {mfs_path} in MFS")
except requests.exceptions.HTTPError as e:
if e.response.status_code != 404:
raise
# Add the CID to MFS so it appears in the Files section
mfs_response = requests.post(f"{IPFS_API_URL}/files/cp?arg=/ipfs/{cid}&arg={mfs_path}")
mfs_response.raise_for_status()
print(f"Added CID to MFS at {mfs_path}")
def publish_to_ipns(cid, key_name):
publish_url = f"{IPFS_API_URL}/name/publish"
params = {
'arg': f'/ipfs/{cid}',
'key': key_name,
'allow-offline': True,
'lifetime': '60m',
'ttl': '10s',
'resolve': True,
'force': True
}
response = requests.post(publish_url, params=params)
response.raise_for_status()
ipns_hash = response.json().get('Name')
print(f"Published to IPNS: {ipns_hash} with CID: {cid}")
# Verify the new CID
time.sleep(5)
resolve_ipns(ipns_hash)
resolve_ipns_locally(ipns_hash)
def resolve_ipns(ipns_hash):
resolve_url = f"{IPFS_API_URL}/name/resolve?arg=/ipns/{ipns_hash}&nocache=true"
response = requests.post(resolve_url)
response.raise_for_status()
resolved_cid = response.json().get('Path')
print(f"Resolved IPNS to CID: {resolved_cid}")
def resolve_ipns_locally(ipns_hash):
try:
# Send a POST request to resolve the IPNS hash using the IPFS HTTP API
response = requests.post(f"http://localhost:5001/api/v0/name/resolve?arg=/ipns/{ipns_hash}&nocache=true")
# Raise an exception if the request failed
response.raise_for_status()
# Extract the resolved CID from the response
resolved_cid = response.json().get('Path')
# Print the resolved CID
print(f"Local IPNS resolution result: {resolved_cid}")
except requests.exceptions.RequestException as e:
# Catch and print any errors
print(f"Failed to resolve IPNS locally: {str(e)}")
# Manually call pubsub with the new CID
pubsub_publish(cid)
def pubsub_publish(cid):
# Publish the new CID to the pubsub topic using subprocess
pubsub_topic = f"/ipns/{IPNS_NAME}"
pubsub_message = f"New CID: {cid}"
try:
# Create a temporary file with the pubsub message content
with open("cid_message.txt", "w") as f:
f.write(pubsub_message)
# Call the IPFS pubsub pub command with subprocess
subprocess.run(
['ipfs', 'pubsub', 'pub', pubsub_topic, 'cid_message.txt'],
check=True
)
print(f"Published {pubsub_message} to pubsub topic {pubsub_topic}")
except subprocess.CalledProcessError as e:
print(f"Failed to publish to pubsub: {e}")
finally:
# Clean up the temporary file
try:
subprocess.run(['rm', 'cid_message.txt'], check=True)
except Exception as cleanup_err:
print(f"Failed to remove temporary file: {cleanup_err}")
def release_unassociated_eips():
"""
Release all Elastic IPs in the region that are not associated with any EC2 instance.
"""
ec2_client = boto3.client('ec2', region_name=AWS_REGION)
# Describe all EIPs
response = ec2_client.describe_addresses()
for address in response['Addresses']:
allocation_id = address.get('AllocationId')
association_id = address.get('AssociationId')
public_ip = address.get('PublicIp')
# If there is no association ID, the EIP is not in use
if not association_id:
print(f"Releasing unassociated EIP: {public_ip} with Allocation ID: {allocation_id}")
try:
ec2_client.release_address(AllocationId=allocation_id)
print(f"Successfully released unassociated EIP: {public_ip}")
except Exception as e:
print(f"Failed to release EIP {public_ip}: {str(e)}")
if __name__ == "__main__":
if not INSTANCE_ID:
raise SystemExit(
'NINJADOH_EC2_INSTANCE_ID must be set. '
'Create an EC2 instance with ladder2/ladder3 interfaces and set the env var to its instance ID.'
)
if not IPNS_NAME:
raise SystemExit(
'NINJADOH_IPNS_NAME must be set to your IPNS key ID. '
'Generate with: ipfs key gen ninjadoh, then set the env var to the key ID (e.g. k51q...).'
)
# Rotate between ladder2 (eth1) and ladder3 (eth2)
new_ip = rotate_between_ladder2_and_ladder3(INSTANCE_ID)
# Adding sleep to give time for IP reassociation to stabilize
time.sleep(10)
# Fetch all associated IPs
all_eips = get_all_eips(INSTANCE_ID)
if all_eips:
# Generate combined certificate for all associated IPs
generate_combined_certificates(all_eips)
# Publish the new IP to IPFS and IPNS
cid = post_ip_to_ipfs(new_ip, query_path="/k51qzi5uqu5di6najzrxj93bpvomq1zqd37fottriis27yajea2pmrdhoesy2m") # The default path is the local IPNS 'self' key
publish_to_ipns(cid, key_name='self')
# Release any unassociated EIPs
release_unassociated_eips()