convert routeros pppoe service to a derivation
and make it configure itself automatically instead of starting out blank
This commit is contained in:
parent
00aa42b803
commit
16a923f58f
5 changed files with 332 additions and 37 deletions
|
@ -1,28 +1,26 @@
|
||||||
# ppp-server
|
# ppp-server
|
||||||
|
|
||||||
To test a router, we need an upstream connection. In this directory,
|
To test a router, we need an upstream connection. This directory
|
||||||
find
|
contains a derivation to download, start and configure a RouterOS
|
||||||
|
"Cloud Hosted Router" instance in a Qemu VM. It is currently
|
||||||
|
set up for automated tests only, and may require some manual
|
||||||
|
frobbing to run interactively.
|
||||||
|
|
||||||
* chr.sh, a script that will start a RouterOS image in qemu.
|
Note that you need to open some multicast ports if you're using the
|
||||||
Login when prompted, username is "admin", blank password
|
NixOS firewall (or probably, any other firewall). For iptables you can
|
||||||
* routeros.config, a set of commands you can feed into routeros
|
accomplish this by editing your configuration.nix or some module it
|
||||||
to set up PPPoE
|
calls:
|
||||||
|
|
||||||
To get the chr-7.5.img image, visit https://mikrotik.com/download and
|
|
||||||
look in the section titled "Cloud Hosted Router" for "Raw disk image"
|
|
||||||
|
|
||||||
You may need to open your firewall a bit to allow multicast packets
|
```
|
||||||
so that the upstream and the liminix qemu instances may communicate
|
networking.firewall.extraCommands = ''
|
||||||
|
ip46tables -A nixos-fw -m pkttype --pkt-type multicast -p udp --dport 1234:1236 -j nixos-fw-accept
|
||||||
|
'';
|
||||||
|
```
|
||||||
|
|
||||||
config.networking.firewall.extraCommands = ''
|
## Provenance
|
||||||
ip46tables -A nixos-fw -m pkttype --pkt-type multicast -p udp --dport 1234:1236 -j nixos-fw-accept
|
|
||||||
'';
|
|
||||||
|
|
||||||
## To connect to the routeros serial
|
The chr-7.x.img image is taken from https://mikrotik.com/download -
|
||||||
|
look in the section titled "Cloud Hosted Router" for "Raw disk image".
|
||||||
The Qemu instance running RouterOS is headless, but it creates
|
Note that this is proprietary software: please read the license
|
||||||
two unix sockets for serial port and monitor.
|
information and make sure you're using it legally.
|
||||||
|
|
||||||
socat -,raw,echo=0,icanon=0,isig=0,icrnl=0,escape=0x0f tests/support/ppp-server/qemu-console
|
|
||||||
|
|
||||||
socat -,raw,echo=0,icanon=0,isig=0,icrnl=0,escape=0x0f tests/support/ppp-server/qemu-monitor
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
/nix/store/ydwiiagdhczynh2lbqh418rglibv93rv-qemu-host-cpu-only-7.0.0/bin/qemu-kvm \
|
|
||||||
-M q35 -display none \
|
|
||||||
-m 1024 \
|
|
||||||
-accel kvm \
|
|
||||||
-daemonize \
|
|
||||||
-serial unix:qemu-console,server,nowait -monitor unix:qemu-monitor,server,nowait \
|
|
||||||
-drive file=chr-7.5.img,format=raw,if=virtio \
|
|
||||||
-netdev socket,id=access,mcast=230.0.0.1:1234 \
|
|
||||||
-device virtio-net-pci,disable-legacy=on,disable-modern=off,netdev=access,mac=ba:ad:1d:ea:11:02 \
|
|
||||||
-netdev socket,id=world,mcast=230.0.0.1:1236 \
|
|
||||||
-device virtio-net-pci,disable-legacy=on,disable-modern=off,netdev=world,mac=ba:ad:1d:ea:11:01
|
|
53
tests/support/ppp-server/default.nix
Normal file
53
tests/support/ppp-server/default.nix
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
stdenv
|
||||||
|
, python3
|
||||||
|
, qemu
|
||||||
|
, fetchzip
|
||||||
|
, writeShellApplication
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
chr-image = fetchzip {
|
||||||
|
url = "https://download.mikrotik.com/routeros/7.7/chr-7.7.img.zip";
|
||||||
|
hash = "sha256-utBQMUgNvl/UTG+GjnQShlGgVtHmRKtnhSTWW/JyeiY=";
|
||||||
|
curlOpts = "-L";
|
||||||
|
};
|
||||||
|
ros-exec-script = stdenv.mkDerivation {
|
||||||
|
name = "ros-exec-script";
|
||||||
|
src = ./.;
|
||||||
|
buildInputs = [python3];
|
||||||
|
buildPhase = ":";
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cp ros-exec-script.py $out/bin/ros-exec-script
|
||||||
|
chmod +x $out/bin/ros-exec-script
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
routeros = writeShellApplication {
|
||||||
|
name = "routeros";
|
||||||
|
runtimeInputs = [ qemu ros-exec-script ];
|
||||||
|
text = ''
|
||||||
|
RUNTIME_DIRECTORY=$1
|
||||||
|
test -d "$RUNTIME_DIRECTORY" || exit 1
|
||||||
|
${qemu}/bin/qemu-system-x86_64 \
|
||||||
|
-M q35 \
|
||||||
|
-m 1024 \
|
||||||
|
-accel kvm \
|
||||||
|
-display none \
|
||||||
|
-daemonize \
|
||||||
|
-pidfile "$RUNTIME_DIRECTORY/pid" \
|
||||||
|
-serial "unix:$RUNTIME_DIRECTORY/console,server,nowait"\
|
||||||
|
-monitor "unix:$RUNTIME_DIRECTORY/monitor,server,nowait" \
|
||||||
|
-snapshot -drive file=${chr-image}/chr-7.7.img,format=raw,if=virtio \
|
||||||
|
-chardev "socket,path=$RUNTIME_DIRECTORY/qmp,server=on,wait=off,id=qga0" \
|
||||||
|
-device virtio-serial \
|
||||||
|
-device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 \
|
||||||
|
-netdev socket,id=access,mcast=230.0.0.1:1234,localaddr=127.0.0.1 \
|
||||||
|
-device virtio-net-pci,disable-legacy=on,disable-modern=off,netdev=access,mac=ba:ad:1d:ea:11:02 \
|
||||||
|
-netdev socket,id=world,mcast=230.0.0.1:1236,localaddr=127.0.0.1 \
|
||||||
|
-device virtio-net-pci,disable-legacy=on,disable-modern=off,netdev=world,mac=ba:ad:1d:ea:11:01
|
||||||
|
ros-exec-script "$RUNTIME_DIRECTORY/qmp" ${./routeros.config}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
inherit routeros ros-exec-script;
|
||||||
|
}
|
260
tests/support/ppp-server/ros-exec-script.py
Normal file
260
tests/support/ppp-server/ros-exec-script.py
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os,time,base64,json,socket,select,errno,sys
|
||||||
|
# FIXME: this script is adapted from
|
||||||
|
# https://wiki.mikrotik.com/wiki/Manual:CHR#Provisioning
|
||||||
|
|
||||||
|
# I don't know if it is freely usable/redistributable
|
||||||
|
|
||||||
|
class GuestAgent(object):
|
||||||
|
'''
|
||||||
|
Qemu guest agent interface
|
||||||
|
runScript and runFile commands are tailored for ROS agent implementation
|
||||||
|
Transport provided by derived classes (transact method)
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self,**kwargs):
|
||||||
|
# Due to file contents being passed as base64 inside json:
|
||||||
|
# - large chunk sizes may slow down guest-side parsing.
|
||||||
|
# - small chunk sizes result in additional message fragmentation overhead.
|
||||||
|
# Default value is a guestimate.
|
||||||
|
self.__chunkSize = kwargs.get('chunkSize', 4096)
|
||||||
|
|
||||||
|
def _qmpError(self,cls,msg):
|
||||||
|
''' Generic callback to log qmp errors before (optionally) raising an exception '''
|
||||||
|
print(cls)
|
||||||
|
for line in msg.split('\n'):
|
||||||
|
print(line)
|
||||||
|
# raise RuntimeError()
|
||||||
|
|
||||||
|
def _error(self,msg,*a):
|
||||||
|
''' Generic callback to misc errors before (optionally) raising an exception '''
|
||||||
|
print(msg.format(*a))
|
||||||
|
# raise RuntimeError()
|
||||||
|
|
||||||
|
def _info(self,msg,*a):
|
||||||
|
''' Generic callback to log info '''
|
||||||
|
print(msg.format(*a))
|
||||||
|
|
||||||
|
def _monitorJob(self,pid):
|
||||||
|
''' Block untill script job completes, echo output. Returns None on failure '''
|
||||||
|
ret = self.transact('guest-exec-status',{'pid':pid})
|
||||||
|
if ret is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
while not bool(ret['exited']):
|
||||||
|
time.sleep(1)
|
||||||
|
ret = self.transact('guest-exec-status',{'pid':pid})
|
||||||
|
if ret is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# err-data is never sent
|
||||||
|
out = []
|
||||||
|
if 'out-data' in ret.keys():
|
||||||
|
out = base64.b64decode(ret['out-data']).decode('utf-8').split('\n')
|
||||||
|
if not out[-1]:
|
||||||
|
out = out[:-1]
|
||||||
|
|
||||||
|
exitcode = int(ret['exitcode'])
|
||||||
|
return exitcode, out
|
||||||
|
|
||||||
|
def putFile(self,src,dst):
|
||||||
|
''' Upload file '''
|
||||||
|
src = os.path.expanduser(src)
|
||||||
|
if not os.path.exists(src) or not os.path.isfile(src):
|
||||||
|
self._error('File does not exist: \'{}\'', src)
|
||||||
|
return None
|
||||||
|
|
||||||
|
ret = self.transact('guest-file-open', {'path':dst,'mode':'w'})
|
||||||
|
if ret is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
handle = int(ret)
|
||||||
|
|
||||||
|
file = open(src, 'rb')
|
||||||
|
for chunk in iter(lambda: file.read(self.__chunkSize), b''):
|
||||||
|
count = len(chunk)
|
||||||
|
chunk = base64.b64encode(chunk).decode('ascii')
|
||||||
|
|
||||||
|
ret = self.transact('guest-file-write',{'handle':handle,'buf-b64':chunk,'count':count})
|
||||||
|
if ret is None:
|
||||||
|
return None
|
||||||
|
self.transact('guest-file-flush',{'handle':handle})
|
||||||
|
ret = self.transact('guest-file-close',{'handle':handle})
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getFile(self,src,dst):
|
||||||
|
''' Download file '''
|
||||||
|
dst = os.path.expanduser(dst)
|
||||||
|
|
||||||
|
ret = self.transact('guest-file-open',{'path':src,'mode':'rb'})
|
||||||
|
if ret is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
handle = int(ret)
|
||||||
|
data = ''
|
||||||
|
size = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
ret = self.transact('guest-file-read',{'handle':handle,'count':self.__chunkSize})
|
||||||
|
if ret is None:
|
||||||
|
return None
|
||||||
|
data += ret['buf-b64']
|
||||||
|
size += int(ret['count'])
|
||||||
|
if bool(ret['eof']):
|
||||||
|
break
|
||||||
|
|
||||||
|
ret = self.transact('guest-file-close',{'handle':handle})
|
||||||
|
data = base64.b64decode(data.encode('ascii'))
|
||||||
|
with open(dst,'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def ping(self):
|
||||||
|
ret = self.transact('guest-ping',{})
|
||||||
|
if ret is None:
|
||||||
|
return None
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def runFile(self,fileName):
|
||||||
|
''' Execute file (on guest) as script '''
|
||||||
|
ret = self.transact('guest-exec',{'path':fileName, 'capture-output':True})
|
||||||
|
if ret is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
pid = ret['pid']
|
||||||
|
return self._monitorJob(pid)
|
||||||
|
|
||||||
|
def runSource(self,cmd):
|
||||||
|
''' Execute script '''
|
||||||
|
if isinstance(cmd,list):
|
||||||
|
cmd = '\n'.join(cmd)
|
||||||
|
cmd += '\n'
|
||||||
|
cmd = base64.b64encode(cmd.encode('utf-8')).decode('ascii')
|
||||||
|
|
||||||
|
ret = self.transact('guest-exec',{'input-data':cmd, 'capture-output':True})
|
||||||
|
if ret is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
pid = ret['pid']
|
||||||
|
return self._monitorJob(pid)
|
||||||
|
|
||||||
|
def shutdown(self,mode='powerdown'):
|
||||||
|
'''
|
||||||
|
Execut shutdown command
|
||||||
|
mode == 'reboot' - reboot guest
|
||||||
|
mode == 'shutdown' or mode == 'halt' - shutdown guest
|
||||||
|
'''
|
||||||
|
ret = self.transact('guest-shutdown',{'mode':mode})
|
||||||
|
return ret
|
||||||
|
|
||||||
|
class SocketAgent(GuestAgent):
|
||||||
|
'''
|
||||||
|
GuestAgent using unix/tcp sockets for communication.
|
||||||
|
'''
|
||||||
|
def __init__(self):
|
||||||
|
GuestAgent.__init__(self,chunkSize= 32 * 65536)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unix(dev):
|
||||||
|
''' Connect using unix socket '''
|
||||||
|
self = SocketAgent()
|
||||||
|
self.__af = socket.AF_UNIX
|
||||||
|
self.__args = dev
|
||||||
|
self.__wait = True
|
||||||
|
return self
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def tcp(ip,port,wait = True):
|
||||||
|
''' Connect using tcp socket '''
|
||||||
|
self = SocketAgent()
|
||||||
|
self.__af = socket.AF_INET
|
||||||
|
self.__args = (ip,port)
|
||||||
|
self.__wait = wait
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._sock = socket.socket(self.__af, socket.SOCK_STREAM)
|
||||||
|
if self.__wait:
|
||||||
|
self._info('Waiting for guest ...')
|
||||||
|
# Wait for hyper to create channel
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self._sock.connect(self.__args)
|
||||||
|
break
|
||||||
|
except socket.error as e:
|
||||||
|
print("error connecting", e)
|
||||||
|
if e.errno == errno.EHOSTUNREACH or e.errno == errno.ECONNREFUSED:
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
self._sock.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
#Wait for guest agent to initialize and sync
|
||||||
|
while True:
|
||||||
|
import random
|
||||||
|
key = random.randint(0, 0xffffffff)
|
||||||
|
msg = json.dumps({'execute':'guest-sync-delimited','arguments':{'id':key}},separators=(',',':'),sort_keys=True)
|
||||||
|
self._sock.send(msg.encode('ascii'))
|
||||||
|
|
||||||
|
self._sock.setblocking(0)
|
||||||
|
response = b''
|
||||||
|
if (select.select([self._sock],[],[])[0]):
|
||||||
|
response += self._sock.recv(65536)
|
||||||
|
else:
|
||||||
|
raise RuntimeError()
|
||||||
|
self._sock.setblocking(1)
|
||||||
|
|
||||||
|
sentinel = b'\xff'
|
||||||
|
response = response.split(sentinel)[-1]
|
||||||
|
if not response:
|
||||||
|
time.sleep(3)
|
||||||
|
continue
|
||||||
|
response = json.loads(response.decode('utf-8').strip())
|
||||||
|
if 'return' in response.keys():
|
||||||
|
if int(response['return']) == key:
|
||||||
|
break
|
||||||
|
time.sleep(3)
|
||||||
|
else:
|
||||||
|
self._sock.connect(self.__args)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self,*a):
|
||||||
|
self._sock.close()
|
||||||
|
|
||||||
|
def transact(self,cmd,args={}):
|
||||||
|
''' Exchange a single command with guest agent '''
|
||||||
|
timeout = 2
|
||||||
|
msg = json.dumps({'execute':cmd,'arguments':args},separators=(',',':'),sort_keys=True)
|
||||||
|
self._sock.send(msg.encode('ascii'))
|
||||||
|
self._sock.setblocking(0)
|
||||||
|
response = b''
|
||||||
|
if (select.select([self._sock],[],[],timeout)[0]):
|
||||||
|
response += self._sock.recv(65536)
|
||||||
|
self._sock.setblocking(1)
|
||||||
|
if not response:
|
||||||
|
response = None
|
||||||
|
else:
|
||||||
|
if response[0] == 255: # sync
|
||||||
|
response = response[1:]
|
||||||
|
print(response.decode('utf-8').strip())
|
||||||
|
response = json.loads(response.decode('utf-8').strip())
|
||||||
|
if 'error' in response.keys():
|
||||||
|
self._qmpError(response['error']['class'],response['error']['desc'])
|
||||||
|
response = None
|
||||||
|
elif 'return' in response:
|
||||||
|
response = response['return']
|
||||||
|
return response
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
socketpath,filename=sys.argv[1:]
|
||||||
|
script = open(filename,"r").readlines()
|
||||||
|
|
||||||
|
with SocketAgent.unix(socketpath) as agent:
|
||||||
|
ret,out = agent.runSource(script)
|
||||||
|
print('ret = {}'.format(ret))
|
||||||
|
for line in out:
|
||||||
|
print(line)
|
|
@ -6,8 +6,6 @@
|
||||||
/interface ethernet
|
/interface ethernet
|
||||||
set [ find default-name=ether1 ] disable-running-check=no name=access
|
set [ find default-name=ether1 ] disable-running-check=no name=access
|
||||||
set [ find default-name=ether2 ] disable-running-check=no name=world
|
set [ find default-name=ether2 ] disable-running-check=no name=world
|
||||||
/disk
|
|
||||||
set sata1 disabled=no
|
|
||||||
/interface wireless security-profiles
|
/interface wireless security-profiles
|
||||||
set [ find default=yes ] supplicant-identity=MikroTik
|
set [ find default=yes ] supplicant-identity=MikroTik
|
||||||
/ip pool
|
/ip pool
|
||||||
|
@ -18,7 +16,5 @@ set 0 name=serial0
|
||||||
add local-address=192.168.100.1 name=pppoe-profile remote-address=pppoe-pool
|
add local-address=192.168.100.1 name=pppoe-profile remote-address=pppoe-pool
|
||||||
/interface pppoe-server server
|
/interface pppoe-server server
|
||||||
add default-profile=pppoe-profile disabled=no interface=access service-name=internet
|
add default-profile=pppoe-profile disabled=no interface=access service-name=internet
|
||||||
/ip dhcp-client
|
|
||||||
add interface=*1
|
|
||||||
/ppp secret
|
/ppp secret
|
||||||
add name=db123@a.1 password=NotReallyTheSecret profile=pppoe-profile service=pppoe
|
add name=db123@a.1 password=NotReallyTheSecret profile=pppoe-profile service=pppoe
|
||||||
|
|
Loading…
Reference in a new issue