Initial commit
This commit is contained in:
commit
eb5b09c6df
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
.venv
|
||||
*.pyc
|
||||
config.yaml
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Schwarzschild
|
||||
|
||||
A little tool that automatically updates the forwarding table in a fritz.box when a system goes down.
|
41
example_config.yaml
Normal file
41
example_config.yaml
Normal file
@ -0,0 +1,41 @@
|
||||
ip: 192.168.3.1
|
||||
user: schwarzschild
|
||||
pass: <pass>
|
||||
period: 10
|
||||
validate_paths_every: 10
|
||||
routes:
|
||||
- name: main
|
||||
paths:
|
||||
- name: dodox-Y700-WIRED
|
||||
ip: 192.168.7.150
|
||||
forwards:
|
||||
- name: haProxy
|
||||
protocol: TCP
|
||||
internal: 809
|
||||
external: 80
|
||||
- name: haProxy_ssl
|
||||
protocol: TCP
|
||||
internal: 4439
|
||||
external: 443
|
||||
- name: dodox-Y700
|
||||
ip: 192.168.3.150
|
||||
forwards:
|
||||
- name: haProxy
|
||||
protocol: TCP
|
||||
internal: 809
|
||||
external: 80
|
||||
- name: haProxy_ssl
|
||||
protocol: TCP
|
||||
internal: 4439
|
||||
external: 443
|
||||
- name: ProjectHeimdall
|
||||
ip: 192.168.3.250
|
||||
forwards:
|
||||
- name: haProxy
|
||||
protocol: TCP
|
||||
internal: 80
|
||||
external: 80
|
||||
- name: haProxy_ssl
|
||||
protocol: TCP
|
||||
internal: 443
|
||||
external: 443
|
218
main.py
Normal file
218
main.py
Normal file
@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import traceback
|
||||
import sys
|
||||
import socket
|
||||
import os
|
||||
import yaml
|
||||
import time
|
||||
|
||||
from ping3 import ping
|
||||
|
||||
import fritzconnection.core.exceptions as exp
|
||||
from fritzconnection.cli import utils
|
||||
from fritzconnection import FritzConnection
|
||||
from fritzconnection.core.fritzconnection import (
|
||||
FRITZ_IP_ADDRESS,
|
||||
FRITZ_TCP_PORT,
|
||||
FRITZ_ENV_USERNAME,
|
||||
FRITZ_ENV_PASSWORD,
|
||||
)
|
||||
|
||||
DRY = True
|
||||
|
||||
def readPortmappings(fc):
|
||||
num = fc.call_action("WANPPPConnection1", "GetPortMappingNumberOfEntries", )
|
||||
portMappings = []
|
||||
numi = num["NewPortMappingNumberOfEntries"]
|
||||
for i in range(numi):
|
||||
portMappings.append(fc.call_action("WANPPPConnection1", "GetGenericPortMappingEntry", NewPortMappingIndex=i))
|
||||
return portMappings
|
||||
|
||||
def findPortmapping(fc, extPort, intPort):
|
||||
portmappings = readPortmappings(fc)
|
||||
ip = readMyIP()
|
||||
for m in portmappings:
|
||||
if m["NewInternalClient"] == ip and m["NewInternalPort"] == int(intPort) and m["NewExternalPort"] == int(extPort):
|
||||
return m
|
||||
return None
|
||||
|
||||
def printMappingHeader():
|
||||
print('{:<20} {:<8} {:<20} {:<20} {:<15} {:<15} {}\n'.format(
|
||||
'Description', 'Protocol', 'Dest. Host', 'Dest. IP', 'Mapping', 'Lease Duration', 'Status'))
|
||||
|
||||
def printMapping(m):
|
||||
desc = str(m['NewPortMappingDescription']) if m['NewPortMappingDescription'] else '-'
|
||||
proto = str(m['NewProtocol']) if m['NewProtocol'] else '-'
|
||||
host = str(m['NewInternalClient']) if m['NewInternalClient'] else '-'
|
||||
extport = str(m['NewExternalPort']) if m['NewExternalPort'] else "?"
|
||||
intport = str(m['NewInternalPort']) if m['NewInternalPort'] else "?"
|
||||
lease = str(m['NewLeaseDuration']) if m['NewLeaseDuration'] else "infinite"
|
||||
status = 'active' if m['NewEnabled'] else ''
|
||||
ip = host
|
||||
mapping = extport + "->" + intport
|
||||
try:
|
||||
host = socket.gethostbyaddr(host)[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f'{desc:<20} {proto:<8} {host:<20} {ip:<20} {mapping:<15} {lease:<15} {status}')
|
||||
|
||||
def listPortmappings(fc):
|
||||
print('Fritz Port Forwardings: \n')
|
||||
printMappingHeader()
|
||||
|
||||
portmappings = readPortmappings(fc)
|
||||
for m in portmappings: printMapping(m)
|
||||
print()
|
||||
|
||||
def readMyIP():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
return s.getsockname()[0]
|
||||
|
||||
def addPortMapping(fc, extPort, intPort, name=None, protocol='TCP', active=True):
|
||||
mapping = {
|
||||
'NewRemoteHost': '0.0.0.0',
|
||||
'NewExternalPort': extPort,
|
||||
'NewProtocol': protocol,
|
||||
'NewInternalPort': intPort,
|
||||
'NewInternalClient': readMyIP(),
|
||||
'NewEnabled': active,
|
||||
'NewPortMappingDescription': name,
|
||||
'NewLeaseDuration': 0
|
||||
}
|
||||
if DRY:
|
||||
print('DRY', mapping)
|
||||
return
|
||||
fc.call_action("WANPPPConnection1", "AddPortMapping", **mapping )
|
||||
|
||||
def deletePortMapping(fc, extPort, intPort, protocol='TCP', name=None):
|
||||
addPortMapping(fc, extPort, intPort, name=name, protocol=protocol, active=False)
|
||||
|
||||
def load_config():
|
||||
with open('config.yaml', 'r') as f:
|
||||
conf = yaml.safe_load(f)
|
||||
return conf
|
||||
|
||||
def make_con(ip, user, pw):
|
||||
return FritzConnection(address=ip, user=user, password=pw, use_cache=True)
|
||||
|
||||
def main():
|
||||
conf = load_config()
|
||||
fc = make_con(conf['ip'], conf['user'], conf['pass'])
|
||||
loop(conf, fc)
|
||||
|
||||
def loop(conf, fc):
|
||||
period = conf['period']
|
||||
active_paths = read_paths(conf, fc)
|
||||
clock = 0
|
||||
info('starting...')
|
||||
while True:
|
||||
begin = time.time()
|
||||
active_paths, clock = cycle(active_paths, clock, conf, fc)
|
||||
sleep = time.time() - begin + period
|
||||
if sleep > 0:
|
||||
time.sleep(sleep)
|
||||
|
||||
def cycle(active_paths, clock, conf, fc):
|
||||
if clock == -1 or (conf['validate_paths_every'] != 0 and clock >= conf['validate_paths_every']-1):
|
||||
clock = 0
|
||||
real_active_paths = read_paths(conf, fc)
|
||||
diff = paths_diffs(active_paths, real_active_paths)
|
||||
if diff:
|
||||
active_paths = real_active_paths
|
||||
alert(f'The paths configured in the router are different from what we expect based on our state. Were they changed manually? We will fix this.')
|
||||
else:
|
||||
info(f'Router paths validated.')
|
||||
else:
|
||||
clock += 1
|
||||
new_paths = select_paths(conf, fc)
|
||||
diff = paths_diffs(active_paths, new_paths)
|
||||
if diff:
|
||||
flush_paths(conf, fc, diff)
|
||||
for route_name in diff:
|
||||
old_path = active_paths[route_name]
|
||||
new_path = new_paths[route_name]
|
||||
alert(f'Route change: Switched from path {old_path} to path {new_path} for route {route_name}')
|
||||
clock = -1 # force check next round
|
||||
info(f'Routes ok:')
|
||||
for route in conf['routes']:
|
||||
print(f' - {route["name"]} -> {new_paths[route["name"]]}')
|
||||
return new_paths, clock
|
||||
|
||||
def paths_diffs(old_paths, new_paths, verbose=False):
|
||||
diffs = {}
|
||||
for route_name in new_paths:
|
||||
if route_name not in old_paths or old_paths[route_name] != new_paths[route_name]:
|
||||
diffs[route_name] = new_paths[route_name]
|
||||
return diffs
|
||||
|
||||
def select_paths(conf, fc):
|
||||
path_indices = {}
|
||||
for route in conf['routes']:
|
||||
for path in route['paths']:
|
||||
if check_avail(path):
|
||||
path_indices[route['name']] = path['name']
|
||||
break
|
||||
else:
|
||||
# All paths are down
|
||||
path_indices[route['name']] = None
|
||||
return path_indices
|
||||
|
||||
def read_paths(conf, fc):
|
||||
ms = readPortmappings(fc)
|
||||
path_indices = {}
|
||||
for route in conf['routes']:
|
||||
for path in route['paths']:
|
||||
# Lets see if we have such a path active
|
||||
fstFwd = path['forwards'][0]
|
||||
found_map = False
|
||||
for m in ms:
|
||||
if m['NewEnabled'] and m['NewInternalClient'] == path['ip'] and m['NewPortMappingDescription'] == fstFwd['name'] and m['NewExternalPort'] == fstFwd['external'] and m['NewInternalPort'] == fstFwd['internal'] and m['NewProtocol'] == fstFwd['protocol'].upper():
|
||||
# This path is (at least partially) active
|
||||
path_indices[route['name']] = path['name']
|
||||
found_map = True
|
||||
break
|
||||
if found_map:
|
||||
break
|
||||
else:
|
||||
# All paths are down
|
||||
path_indices[route['name']] = None
|
||||
alert('We found no active path for route {route["name"]}.')
|
||||
return path_indices
|
||||
|
||||
def flush_paths(conf, fc, active_paths):
|
||||
for route in conf['routes']:
|
||||
if route['name'] not in active_paths:
|
||||
continue
|
||||
active_path = active_paths[route['name']]
|
||||
make_active_path = None
|
||||
for path in route['paths']:
|
||||
if path['name'] == active_path:
|
||||
if not make_active_path == None:
|
||||
alert(f'Illegal config for {route["name"]}: Has redundant path names.')
|
||||
make_active_path = path
|
||||
else:
|
||||
flush_forwards(path, False, fc)
|
||||
flush_forwards(make_active_path, True, fc)
|
||||
if make_active_path==None:
|
||||
alert(f'No active path for route {route["name"]}')
|
||||
|
||||
def flush_forwards(path, make_active, fc):
|
||||
for forward in path['forwards']:
|
||||
addPortMapping(fc, forward['external'], forward['internal'], name=forward['name'], protocol=forward['protocol'], active=make_active)
|
||||
|
||||
def check_avail(path):
|
||||
ip = path['ip']
|
||||
resp_time = ping(ip, timeout=3)
|
||||
return resp_time!=None
|
||||
|
||||
def alert(txt):
|
||||
print('[!] '+txt)
|
||||
|
||||
def info(txt):
|
||||
print('[i] '+txt)
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
Loading…
Reference in New Issue
Block a user