├── .gitignore ├── version.txt ├── ubuntu-auto-install ├── meta-data ├── vendor-data ├── user-data └── README.md ├── ripe-app-frontend ├── .gitignore ├── app │ ├── workspaces │ │ ├── 404 │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── network │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── network_diagram.svg │ │ ├── legal │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── admin │ │ │ ├── index.js │ │ │ └── index.html │ │ └── dashboard │ │ │ ├── index.js │ │ │ └── index.html │ ├── components │ │ ├── scratch-pad.html │ │ ├── vm-console.html │ │ ├── vm-console.js │ │ └── scratch-pad.js │ ├── styles │ │ └── styles.css │ ├── index.js │ ├── menu.json │ └── router.js ├── build │ ├── app_workspaces_404_index_html.bundle.js │ ├── app_workspaces_404_index_js.bundle.js │ ├── app_workspaces_legal_index_js.bundle.js │ ├── app_workspaces_network_index_js.bundle.js │ ├── app_workspaces_legal_index_html.bundle.js │ ├── app_workspaces_admin_index_js.bundle.js │ ├── app_components_scratch-pad_html.bundle.js │ ├── app_workspaces_dashboard_index_js.bundle.js │ ├── app_workspaces_admin_index_html.bundle.js │ ├── app_workspaces_network_index_html.bundle.js │ ├── app_components_vm-console_js.bundle.js │ ├── app_components_scratch-pad_js.bundle.js │ ├── app_components_vm-console_html.bundle.js │ ├── app_workspaces_dashboard_index_html.bundle.js │ ├── main.css │ ├── index.html │ └── 38f376c724439f780378.svg ├── README.md ├── package.json ├── webpack.config.js └── html │ └── index.html ├── ansible ├── files │ ├── nginx │ │ ├── connection_upgrade.conf │ │ └── ttydconsole.conf │ ├── tmux.conf │ └── lxd_preseed.yaml ├── templates │ └── systemd │ │ ├── ttyd-container.target │ │ ├── ttyd-container.generator │ │ ├── ttyd-admin.service │ │ └── ttyd-container@.service ├── inventory.ini └── site.yaml ├── .github └── workflows │ ├── release-please.yml │ └── vagrant-build.yml ├── Vagrantfile-in-box ├── Vagrantfile ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 0.20240816.0 2 | -------------------------------------------------------------------------------- /ubuntu-auto-install/meta-data: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ubuntu-auto-install/vendor-data: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ripe-app-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/404/index.html: -------------------------------------------------------------------------------- 1 |
2 |

Not found

3 |
4 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/network/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /ansible/files/nginx/connection_upgrade.conf: -------------------------------------------------------------------------------- 1 | map $http_upgrade $connection_upgrade { 2 | default upgrade; 3 | '' close; 4 | } 5 | -------------------------------------------------------------------------------- /ansible/templates/systemd/ttyd-container.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ttyd-container target 3 | 4 | [Install] 5 | WantedBy=default.target 6 | -------------------------------------------------------------------------------- /ansible/inventory.ini: -------------------------------------------------------------------------------- 1 | default ansible_connection=local 2 | 3 | [containers] 4 | hostA 5 | hostB 6 | hostC 7 | 8 | [containers:vars] 9 | ansible_connection=lxd 10 | 11 | [all:vars] 12 | ansible_python_interpreter=/usr/bin/python3 13 | -------------------------------------------------------------------------------- /ansible/files/tmux.conf: -------------------------------------------------------------------------------- 1 | #unbind-key -a 2 | #set-option -g status off 3 | set-option -g set-titles on 4 | set-option -g set-titles-string "#S" 5 | set-option -g terminal-overrides "xterm*:smcup@:rmcup@" 6 | set-option -s default-terminal "screen-256color" 7 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/legal/index.html: -------------------------------------------------------------------------------- 1 | 10 | 13 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/404/index.js: -------------------------------------------------------------------------------- 1 | export const loadWorkSpace = async () => { 2 | const wsElm = document.querySelector("div#csr-workspace"); 3 | 4 | // put template in the DOM first. 5 | const wsContent = await import("./index.html"); 6 | wsElm.innerHTML = `${wsContent.default}`; 7 | }; 8 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/legal/index.js: -------------------------------------------------------------------------------- 1 | export const loadWorkSpace = async () => { 2 | const wsElm = document.querySelector("div#csr-workspace"); 3 | 4 | // put template in the DOM first. 5 | const wsContent = await import("./index.html"); 6 | wsElm.innerHTML = `${wsContent.default}`; 7 | }; 8 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/network/index.js: -------------------------------------------------------------------------------- 1 | export const loadWorkSpace = async () => { 2 | const wsElm = document.querySelector("div#csr-workspace"); 3 | 4 | // put template in the DOM first. 5 | const wsContent = await import("./index.html"); 6 | wsElm.innerHTML = wsContent.default; 7 | }; 8 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_404_index_html.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_404_index_html"],{"./app/workspaces/404/index.html":(e,s,n)=>{n.r(s),n.d(s,{default:()=>h});const h="
\n

Not found

\n
\n"}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/admin/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export const loadWorkSpace = async () => { 4 | await import("../../components/vm-console"); 5 | const wsElm = document.querySelector("div#csr-workspace"); 6 | const wsContent = await import("./index.html"); 7 | wsElm.innerHTML = `${wsContent.default}`; 8 | }; 9 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/dashboard/index.js: -------------------------------------------------------------------------------- 1 | export const loadWorkSpace = async () => { 2 | await import("../../components/vm-console"); 3 | await import("../../components/scratch-pad"); 4 | const wsElm = document.querySelector("div#csr-workspace"); 5 | const wsContent = await import("./index.html"); 6 | wsElm.innerHTML = `${wsContent.default}`; 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release-please: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: GoogleCloudPlatform/release-please-action@v2 13 | with: 14 | release-type: simple 15 | package-name: ipv6-security-lab 16 | -------------------------------------------------------------------------------- /ansible/templates/systemd/ttyd-container.generator: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | 4 | GENDIR="$1" 5 | WANTDIR="$1/ttyd-container.target.wants" 6 | SERVICEFILE="/etc/systemd/system/ttyd-container@.service" 7 | 8 | mkdir -p "$WANTDIR" 9 | 10 | for INSTANCE in {% for host in groups['containers'] %}{{host}} {% endfor %}; do 11 | ln -s "$SERVICEFILE" "$WANTDIR/ttyd-container@$INSTANCE.service" 12 | done 13 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/components/scratch-pad.html: -------------------------------------------------------------------------------- 1 | 12 |

Scratchpad

13 | 14 | -------------------------------------------------------------------------------- /ansible/templates/systemd/ttyd-admin.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ttyd admin instance 3 | 4 | [Service] 5 | User={{ ansible_env.SUDO_USER or ansible_user_id }} 6 | Group=www-data 7 | RuntimeDirectory=ttyd-admin 8 | UMask=0002 9 | ExecStart=/opt/ttyd -i /run/ttyd-admin/admin.sock -t fontSize=14 -t disableLeaveAlert=true tmux -2 new -A -D -s admin 10 | Type=simple 11 | Restart=always 12 | 13 | [Install] 14 | WantedBy=default.target 15 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_404_index_js.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_404_index_js"],{"./app/workspaces/404/index.js":(e,s,a)=>{a.r(s),a.d(s,{loadWorkSpace:()=>c});const c=async()=>{const e=document.querySelector("div#csr-workspace"),s=await a.e("app_workspaces_404_index_html").then(a.bind(a,"./app/workspaces/404/index.html"));e.innerHTML=`${s.default}`}}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_legal_index_js.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_legal_index_js"],{"./app/workspaces/legal/index.js":(e,s,a)=>{a.r(s),a.d(s,{loadWorkSpace:()=>l});const l=async()=>{const e=document.querySelector("div#csr-workspace"),s=await a.e("app_workspaces_legal_index_html").then(a.bind(a,"./app/workspaces/legal/index.html"));e.innerHTML=`${s.default}`}}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_network_index_js.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_network_index_js"],{"./app/workspaces/network/index.js":(e,s,n)=>{n.r(s),n.d(s,{loadWorkSpace:()=>r});const r=async()=>{const e=document.querySelector("div#csr-workspace"),s=await n.e("app_workspaces_network_index_html").then(n.bind(n,"./app/workspaces/network/index.html"));e.innerHTML=s.default}}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_legal_index_html.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_legal_index_html"],{"./app/workspaces/legal/index.html":(e,l,n)=>{n.r(l),n.d(l,{default:()=>s});const s='\n\n'}}]); -------------------------------------------------------------------------------- /ansible/templates/systemd/ttyd-container@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ttyd-container instance %I 3 | 4 | [Service] 5 | User={{ ansible_env.SUDO_USER or ansible_user_id }} 6 | Group=www-data 7 | RuntimeDirectory=ttyd-container 8 | UMask=0002 9 | ExecStart=/opt/ttyd -i /run/ttyd-container/%i.sock -t fontSize=14 -t disableLeaveAlert=true tmux -2 -L %i set -g default-command "lxc exec %i -t -- bash" \; new -A -D -s %i 10 | Type=simple 11 | Restart=always 12 | 13 | [Install] 14 | WantedBy=default.target 15 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_admin_index_js.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_admin_index_js"],{"./app/workspaces/admin/index.js":(e,n,s)=>{s.r(n),s.d(n,{loadWorkSpace:()=>a});const a=async()=>{await s.e("app_components_vm-console_js").then(s.t.bind(s,"./app/components/vm-console.js",23));const e=document.querySelector("div#csr-workspace"),n=await s.e("app_workspaces_admin_index_html").then(s.bind(s,"./app/workspaces/admin/index.html"));e.innerHTML=`${n.default}`}}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/admin/index.html: -------------------------------------------------------------------------------- 1 | 15 |
16 | 17 |

Admin console

18 |
19 |
20 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_components_scratch-pad_html.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_components_scratch-pad_html"],{"./app/components/scratch-pad.html":(e,t,n)=>{n.r(t),n.d(t,{default:()=>a});const a='\n

Scratchpad

\n\n'}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/README.md: -------------------------------------------------------------------------------- 1 | How to compile HTML frontend 2 | ============================ 3 | 4 | **WARNING:** The build process depends on internal RIPE NCC components. 5 | Do not attempt to build it outside RIPE NCC, you might accidentally inject 6 | malicious code from a public repository of the same name. 7 | 8 | 9 | 1. Install [Node.js](https://nodejs.org/) 10 | 2. Setup **internal components registry** path in `~/.npmrc` 11 | 3. Run `npm install` 12 | 4. Run development server on port 4042 by running `npm run start`. It will 13 | proxy console access to `http://localhost:8080/`. 14 | 5. Build a new version by running `npm run build` 15 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_dashboard_index_js.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_dashboard_index_js"],{"./app/workspaces/dashboard/index.js":(s,a,e)=>{e.r(a),e.d(a,{loadWorkSpace:()=>p});const p=async()=>{await e.e("app_components_vm-console_js").then(e.t.bind(e,"./app/components/vm-console.js",23)),await e.e("app_components_scratch-pad_js").then(e.t.bind(e,"./app/components/scratch-pad.js",23));const s=document.querySelector("div#csr-workspace"),a=await e.e("app_workspaces_dashboard_index_html").then(e.bind(e,"./app/workspaces/dashboard/index.html"));s.innerHTML=`${a.default}`}}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_admin_index_html.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_admin_index_html"],{"./app/workspaces/admin/index.html":(n,e,s)=>{s.r(e),s.d(e,{default:()=>o});const o=' \n
\n \n

Admin console

\n
\n
\n'}}]); -------------------------------------------------------------------------------- /ansible/files/lxd_preseed.yaml: -------------------------------------------------------------------------------- 1 | networks: 2 | - type: bridge 3 | config: 4 | ipv4.address: 192.0.2.1/24 5 | ipv4.nat: true 6 | ipv6.address: 2001:db8:f:1::1/64 7 | ipv6.nat: true 8 | ipv6.dhcp: false 9 | raw.dnsmasq: ra-param=lxdbr0,120 10 | bridge.hwaddr: "00:16:3e:ee:00:01" 11 | description: "Default network" 12 | name: lxdbr0 13 | storage_pools: 14 | - config: 15 | size: 15GB 16 | description: "" 17 | name: default 18 | driver: btrfs 19 | profiles: 20 | - config: {} 21 | description: "" 22 | devices: 23 | eth0: 24 | name: eth0 25 | nictype: bridged 26 | parent: lxdbr0 27 | type: nic 28 | root: 29 | path: / 30 | pool: default 31 | type: disk 32 | name: default 33 | -------------------------------------------------------------------------------- /Vagrantfile-in-box: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | # Create a forwarded port mapping which allows access to a specific port 6 | # within the machine from a port on the host machine and only allow access 7 | # via 127.0.0.1 to disable public access 8 | config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1", id: "web" 9 | 10 | # Disable shared folder to avoid scary warning 11 | config.vm.synced_folder ".", "/vagrant", disabled: true 12 | 13 | # Workaround issue with Windows, see: 14 | # https://github.com/hashicorp/vagrant/issues/8604 15 | config.vm.provider "virtualbox" do |vb| 16 | vb.customize [ "modifyvm", :id, "--uartmode1", "disconnected" ] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_network_index_html.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_network_index_html"],{"./app/workspaces/network/index.html":(e,s,t)=>{t.r(s),t.d(s,{default:()=>a});var n=t("./node_modules/html-loader/dist/runtime/getUrl.js"),r=t.n(n),o=new URL(t("./app/workspaces/network/network_diagram.svg"),t.b);const a='
\n \n
\n'},"./node_modules/html-loader/dist/runtime/getUrl.js":e=>{e.exports=function(e,s){return s||(s={}),e?(e=String(e.__esModule?e.default:e),s.hash&&(e+=s.hash),s.maybeNeedQuotes&&/[\t\n\f\r "'=<>`]/.test(e)?'"'.concat(e,'"'):e):e}},"./app/workspaces/network/network_diagram.svg":(e,s,t)=>{e.exports=t.p+"38f376c724439f780378.svg"}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_components_vm-console_js.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_components_vm-console_js"],{"./app/components/vm-console.js":(e,t,o)=>{if(!customElements.get("vm-console")){let e=document.createElement("template");o.e("app_components_vm-console_html").then(o.bind(o,"./app/components/vm-console.html")).then((t=>{e.innerHTML=`${t.default}`,customElements.define("vm-console",class extends HTMLElement{constructor(){super();const t=e.content.cloneNode(!0),o=t.querySelector("iframe");t.getElementById("refresher").onclick=()=>(o.contentDocument.location.reload(),!1),this.attachShadow({mode:"open"}).appendChild(t)}static get observedAttributes(){return["src"]}attributeChangedCallback(e,t,o){"src"==e&&(this.shadowRoot.getElementById("popout").href=o,this.shadowRoot.querySelector("iframe").src=o)}})}))}}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workbench-shell", 3 | "version": "0.1", 4 | "description": "The locally-installed RIPE NCC Workbench", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack serve --progress --port 4042", 8 | "build:clean": "rimraf build/*", 9 | "build": "webpack" 10 | }, 11 | "keywords": [ 12 | "RIPE NCC", 13 | "Workbench" 14 | ], 15 | "author": "RIPE NCC L&D", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@technical-design/ripe-app-webcomponents": "^1.5.3", 19 | "copy-webpack-plugin": "^9.0.1", 20 | "css-loader": "^6.2.0", 21 | "html-loader": "^2.1.2", 22 | "mini-css-extract-plugin": "^2.2.0", 23 | "style-loader": "^3.2.1", 24 | "webpack": "^5.50.0", 25 | "webpack-dev-server": "^4.3.1" 26 | }, 27 | "devDependencies": { 28 | "css-minimizer-webpack-plugin": "^3.0.2", 29 | "webpack-cli": "^4.8.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/components/vm-console.html: -------------------------------------------------------------------------------- 1 | 27 |
28 | 29 | reconnect 30 | pop out 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_components_scratch-pad_js.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_components_scratch-pad_js"],{"./app/components/scratch-pad.js":(e,t,s)=>{if(!customElements.get("scratch-pad")){let e=document.createElement("template");s.e("app_components_scratch-pad_html").then(s.bind(s,"./app/components/scratch-pad.html")).then((t=>{e.innerHTML=`${t.default}`,customElements.define("scratch-pad",class extends HTMLElement{constructor(){super();const t=e.content.cloneNode(!0);this.scratchPad=t.querySelector("textarea"),this.attachShadow({mode:"open"}).appendChild(t),this.storage_key="ripencc-workbench-scratchpad",this.load_notes()}static get observedAttributes(){return["id"]}attributeChangedCallback(e,t,s){"id"==e&&(this.storage_key=s,this.load_notes())}load_notes(){const e=sessionStorage.getItem(this.storage_key);null!=e&&(this.scratchPad.value=e),this.scratchPad.onchange=()=>{sessionStorage.setItem(this.storage_key,this.scratchPad.value)}}})}))}}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_components_vm-console_html.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_components_vm-console_html"],{"./app/components/vm-console.html":(n,e,o)=>{o.r(e),o.d(e,{default:()=>i});const i='\n
\n \n reconnect\n pop out\n
\n
\n
\n'}}]); -------------------------------------------------------------------------------- /ripe-app-frontend/app/components/vm-console.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | if (!customElements.get("vm-console")) { 4 | let vcTemplate = document.createElement("template"); 5 | import("./vm-console.html") 6 | .then((content) => { 7 | vcTemplate.innerHTML = `${content.default}` 8 | customElements.define( 9 | "vm-console", 10 | class extends HTMLElement { 11 | constructor() { 12 | super(); 13 | const vcClone = vcTemplate.content.cloneNode(true); 14 | const vcIframe = vcClone.querySelector("iframe"); 15 | vcClone.getElementById("refresher").onclick = () => { 16 | vcIframe.contentDocument.location.reload(); 17 | return false; 18 | }; 19 | this.attachShadow({ mode: "open" }).appendChild(vcClone); 20 | } 21 | static get observedAttributes() { 22 | return ["src"]; 23 | } 24 | attributeChangedCallback(name, oldValue, newValue) { 25 | if (name == "src") { 26 | this.shadowRoot.getElementById("popout").href = newValue; 27 | this.shadowRoot.querySelector("iframe").src = newValue; 28 | } 29 | } 30 | } 31 | ); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /ansible/files/nginx/ttydconsole.conf: -------------------------------------------------------------------------------- 1 | # Ansible managed 2 | 3 | location ~ ^(/console/[A-Za-z0-9]+)$ { 4 | return 301 $1/; 5 | } 6 | 7 | location ^~ /console/admin/ { 8 | proxy_pass http://unix:/run/ttyd-admin/admin.sock:; 9 | proxy_http_version 1.1; 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_set_header Host $host; 12 | proxy_set_header X-Forwarded-For $remote_addr; 13 | # needed for websocket 14 | proxy_set_header Upgrade $http_upgrade; 15 | proxy_set_header Connection $connection_upgrade; 16 | # websockets can be silent for a while 17 | proxy_read_timeout 3600s; 18 | proxy_send_timeout 3600s; 19 | rewrite ^/console/[A-Za-z0-9]+/?(.*)$ /$1 break; 20 | } 21 | 22 | location ~ ^/console/(?P[a-zA-Z0-9]+)/ { 23 | proxy_pass http://unix:/run/ttyd-container/$router.sock:; 24 | proxy_http_version 1.1; 25 | proxy_set_header X-Real-IP $remote_addr; 26 | proxy_set_header Host $host; 27 | proxy_set_header X-Forwarded-For $remote_addr; 28 | # needed for websocket 29 | proxy_set_header Upgrade $http_upgrade; 30 | proxy_set_header Connection $connection_upgrade; 31 | # websockets can be silent for a while 32 | proxy_read_timeout 3600s; 33 | proxy_send_timeout 3600s; 34 | rewrite ^/console/[A-Za-z0-9]+/?(.*)$ /$1 break; 35 | } 36 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/components/scratch-pad.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | if (!customElements.get("scratch-pad")) { 4 | let spTemplate = document.createElement("template"); 5 | import("./scratch-pad.html") 6 | .then((content) => { 7 | spTemplate.innerHTML = `${content.default}` 8 | customElements.define( 9 | "scratch-pad", 10 | class extends HTMLElement { 11 | constructor() { 12 | super(); 13 | const spClone = spTemplate.content.cloneNode(true); 14 | this.scratchPad = spClone.querySelector("textarea"); 15 | this.attachShadow({ mode: "open" }).appendChild(spClone); 16 | this.storage_key = "ripencc-workbench-scratchpad"; 17 | this.load_notes(); 18 | } 19 | static get observedAttributes() { 20 | return ["id"]; 21 | } 22 | attributeChangedCallback(name, oldValue, newValue) { 23 | if (name == "id") { 24 | this.storage_key = newValue; 25 | this.load_notes(); 26 | } 27 | } 28 | load_notes() { 29 | const scratchPadData = sessionStorage.getItem(this.storage_key); 30 | if (scratchPadData != null) { 31 | this.scratchPad.value = scratchPadData; 32 | } 33 | this.scratchPad.onchange = () => { 34 | sessionStorage.setItem(this.storage_key, this.scratchPad.value); 35 | } 36 | } 37 | } 38 | ); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/dashboard/index.html: -------------------------------------------------------------------------------- 1 | 11 |
12 |

Dashboard

13 |
14 |

Host A

15 |

Host B

16 |

Host C

17 |
18 |

Availaible tools

19 | 25 |

Hints

26 |
    27 |
  • Feel free to resize terminal windows by dragging (does not work in Safari)
  • 28 |
  • To scroll inside the tmux, use Ctrl-B and PageUp/PageDown (Fn + Up/Down on Mac)
  • 29 |
  • To open new tmux window, use Ctrl-B c
  • 30 |
  • See tmux cheatsheet
  • 31 |
32 | 33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/app_workspaces_dashboard_index_html.bundle.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkworkbench_shell=self.webpackChunkworkbench_shell||[]).push([["app_workspaces_dashboard_index_html"],{"./app/workspaces/dashboard/index.html":(e,o,n)=>{n.r(o),n.d(o,{default:()=>s});const s=' \n
\n

Dashboard

\n
\n

Host A

\n

Host B

\n

Host C

\n
\n

Availaible tools

\n \n

Hints

\n
    \n
  • Feel free to resize terminal windows by dragging (does not work in Safari)
  • \n
  • To scroll inside the tmux, use Ctrl-B and PageUp/PageDown (Fn + Up/Down on Mac)
  • \n
  • To open new tmux window, use Ctrl-B c
  • \n
  • See tmux cheatsheet
  • \n
\n \n
\n
\n
\n'}}]); -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure("2") do |config| 9 | config.vm.box = "ubuntu/jammy64" 10 | # config.vm.box_version = "20210603.0.0" 11 | 12 | config.vm.synced_folder ".", "/vagrant", type: "rsync", 13 | rsync__exclude: ".git/" 14 | 15 | # Create a forwarded port mapping which allows access to a specific port 16 | # within the machine from a port on the host machine and only allow access 17 | # via 127.0.0.1 to disable public access 18 | config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1", id: "web" 19 | 20 | # Provider-specific configuration so you can fine-tune various 21 | # backing providers for Vagrant. These expose provider-specific options. 22 | # Example for VirtualBox: 23 | # 24 | # config.vm.provider "virtualbox" do |vb| 25 | # # Display the VirtualBox GUI when booting the machine 26 | # vb.gui = true 27 | # 28 | # # Customize the amount of memory on the VM: 29 | # vb.memory = "1024" 30 | # end 31 | config.vm.provision "shell", reset: true, inline: <<-SHELL 32 | gpasswd -a vagrant lxd 33 | add-apt-repository -y ppa:ansible/ansible 34 | export DEBIAN_FRONTEND=noninteractive 35 | apt-get -y install ansible-core python3-packaging 36 | ansible-galaxy collection install community.general -p /usr/share/ansible/collections 37 | SHELL 38 | config.vm.provision "ansible_local" do |ansible| 39 | ansible.install = false 40 | ansible.playbook = "ansible/site.yaml" 41 | ansible.inventory_path = "ansible/inventory.ini" 42 | ansible.verbose = false 43 | ansible.limit = "all" 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /ubuntu-auto-install/user-data: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | autoinstall: 3 | snaps: 4 | - name: lxd 5 | packages: 6 | - python3-packaging 7 | - python3-pip 8 | - git 9 | identity: 10 | hostname: ipv6seclab 11 | username: ubuntu 12 | password: $6$exDY1mhS4KUYCE/2$zmn9ToZwTKLhCw.b4/b.ZRTIZM30JZ4QrOQ2aOXJ8yk96xpcCof0kxKwuX1kqLG/ygbJ1f8wxED22bTL4F46P0 13 | late-commands: 14 | - "curtin in-target -- bash -c 'mkdir -p /etc/systemd/system/getty@.service.d/; echo -e \"[Service]\nTTYVTDisallocate=no\nExecStart=\nExecStart=-/sbin/agetty -o '\\''-p -- \\\\u'\\'' --autologin ubuntu --noclear %I \\$TERM\" > /etc/systemd/system/getty@.service.d/override.conf'" 15 | - "curtin in-target -- bash -c 'echo -e \"#!/bin/bash\nif [[ \\$USER == ubuntu && \\\"\\$XDG_SESSION_TYPE\\\" == tty ]]; then\n\techo\n\techo -n Please wait until cloud-init finishes with \\\"status: done\\\"\n\tcloud-init status --wait\nfi\" > /etc/profile.d/ZZ-wait-cloud-init.sh'" 16 | user-data: 17 | users: 18 | - name: ubuntu 19 | sudo: ALL=(ALL) NOPASSWD:ALL 20 | runcmd: 21 | - [ passwd, -d, ubuntu ] 22 | ansible: 23 | install_method: pip 24 | package_name: ansible 25 | run_user: ubuntu 26 | galaxy: 27 | actions: 28 | - ["ansible-galaxy", "collection", "install", "community.general", "ansible.posix"] 29 | setup_controller: 30 | repositories: 31 | - path: /home/ubuntu/ipv6-security-lab/ 32 | source: https://github.com/RIPE-NCC/ipv6-security-lab.git 33 | run_ansible: 34 | - playbook_dir: /home/ubuntu/ipv6-security-lab/ansible/ 35 | playbook_name: site.yaml 36 | inventory: inventory.ini 37 | source: 38 | id: ubuntu-server-minimal 39 | search_drivers: false 40 | storage: 41 | swap: 42 | size: 0 43 | layout: 44 | name: direct 45 | updates: all 46 | version: 1 47 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/styles/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --ripe-app-font-family: "Public Sans"; 3 | } 4 | 5 | @media only screen and (max-width: 700px) { 6 | #app-grid { 7 | grid-template-columns: [left] 300px [right] auto; 8 | } 9 | 10 | #app-banners-container { 11 | grid-column: left / 2 span; 12 | top: 40px; 13 | } 14 | 15 | #app-workspace { 16 | grid-column: left / 2 span; 17 | } 18 | } 19 | 20 | @media only screen and (min-width: 701px) { 21 | #app-grid { 22 | grid-template-columns: [left] max-content [right] auto; 23 | } 24 | 25 | #app-banners-container { 26 | grid-column: right; 27 | top: 70px; 28 | } 29 | 30 | #app-workspace { 31 | grid-column: right; 32 | } 33 | } 34 | 35 | @media only screen and (max-width: 400px) { 36 | #atlas-logo-title { 37 | max-width: 100px; 38 | } 39 | } 40 | 41 | #app-grid { 42 | width: 100%; 43 | display: grid; 44 | grid-template-rows: [bc] min-content [ws] auto; 45 | background-color: #fafafa; 46 | } 47 | 48 | app-nav-bar { 49 | grid-column: left; 50 | grid-row: bc / 1000 span; 51 | } 52 | 53 | app-switcher { 54 | font-weight: 400; 55 | } 56 | 57 | #app-banners-container { 58 | position: sticky; 59 | grid-row: bc; 60 | background-color: white; 61 | } 62 | 63 | #app-workspace { 64 | grid-row: ws; 65 | margin: 0; 66 | background-color: var(--ripe-app-main-background, #fafafa); 67 | } 68 | 69 | #csr-workspace { 70 | padding: 24px; 71 | } 72 | 73 | html { 74 | /* overflow-x: hidden; */ 75 | } 76 | 77 | html, 78 | body { 79 | font-family: var(--ripe-app-font-family), sans-serif; 80 | font-size: 16px; 81 | line-height: 1.5rem; 82 | box-sizing: border-box; 83 | margin: 0; 84 | width: 100%; 85 | /* height: 100%; */ 86 | } 87 | 88 | #fb div { 89 | display: inline-block; 90 | } 91 | 92 | .accent { 93 | color: var(--ripe-app-positive); 94 | } 95 | 96 | .alarmed { 97 | color: var(--ripe-app-alarmed); 98 | } 99 | 100 | roll-over a:visited { 101 | color: var(--ripe-app-secondary-20); 102 | } 103 | 104 | roll-over a { 105 | color: var(--ripe-app-secondat-100); 106 | } 107 | 108 | roll-over p { 109 | line-height: 1.4rem; 110 | } 111 | -------------------------------------------------------------------------------- /.github/workflows/vagrant-build.yml: -------------------------------------------------------------------------------- 1 | name: Build a release 2 | # Source: https://github.com/jonashackt/vagrant-github-actions 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'version.txt' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build-release: 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - name: Install Vagrant and Virtualbox 16 | run: | 17 | sudo apt update && sudo apt install virtualbox 18 | wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg 19 | echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list 20 | sudo apt update && sudo apt install vagrant 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | 24 | - name: Cache Vagrant boxes 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.vagrant.d/boxes 28 | key: ${{ runner.os }}-vagrant-${{ hashFiles('Vagrantfile') }} 29 | restore-keys: | 30 | ${{ runner.os }}-vagrant- 31 | 32 | - name: Show Vagrant version 33 | run: vagrant --version 34 | 35 | - name: Show VirtualBox version 36 | run: VBoxManage --version 37 | 38 | - name: Build the environment 39 | run: vagrant up 40 | 41 | - name: Create a Vagrant box 42 | run: vagrant package --vagrantfile Vagrantfile-in-box --output ipv6-security-lab-$(cat version.txt).box 43 | 44 | - name: Store box file 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: vagrant-box 48 | path: "ipv6-security-lab-*.box" 49 | 50 | - name: Publish box file in the release 51 | if: startsWith(github.ref, 'refs/tags/') 52 | uses: djn24/add-asset-to-release@v2 53 | continue-on-error: true 54 | with: 55 | token: ${{ secrets.GITHUB_TOKEN }} 56 | args: "ipv6-security-lab-*.box" 57 | 58 | - name: Publish box file in the Vagrant Cloud 59 | env: 60 | ATLAS_TOKEN: ${{ secrets.VAGRANT_CLOUD_TOKEN }} 61 | run: vagrant cloud publish ripencc/ipv6seclab $(cat version.txt) virtualbox ipv6-security-lab-$(cat version.txt).box --force 62 | -------------------------------------------------------------------------------- /ripe-app-frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const OptimizeCSSAssetsPlugin = require("css-minimizer-webpack-plugin"); 6 | const TerserPlugin = require("terser-webpack-plugin"); 7 | 8 | var dir_app = path.resolve(__dirname, "app"); 9 | var dir_build = path.resolve(__dirname, "build"); 10 | var dir_html = path.resolve(__dirname, "html"); 11 | 12 | module.exports = { 13 | mode: "development", 14 | devtool: "cheap-module-source-map", 15 | devServer: { 16 | static: dir_build, 17 | compress: true, 18 | historyApiFallback: true, 19 | port: 4022, 20 | proxy: [{ 21 | context: ['/console/hostA', '/console/hostB', '/console/hostC', '/console/admin', '/version.txt'], 22 | target: 'http://127.0.0.1:8080', 23 | ws: true 24 | }] 25 | }, 26 | optimization: { 27 | minimize: true, 28 | minimizer: [ 29 | new TerserPlugin({ 30 | terserOptions: { 31 | output: { 32 | comments: false, 33 | }, 34 | }, 35 | extractComments: false, 36 | }), 37 | new OptimizeCSSAssetsPlugin({}), 38 | ], 39 | }, 40 | entry: [path.resolve(dir_app, "index.js")], 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.js[x]?$/, 45 | include: [/app/], 46 | // includes don't work with linked (local) modules, 47 | // such as the @ripe-rnd/ui-components 48 | // so excluding is the way to go. 49 | exclude: /.*node_modules\/((?!@ripe-rnd).)*$/, 50 | }, 51 | { 52 | test: /\.css$/, 53 | use: [ 54 | MiniCssExtractPlugin.loader, // instead of style-loader 55 | "css-loader", 56 | ], 57 | }, 58 | { test: /\.html$/, loader: "html-loader", options: { esModule: true } }, 59 | ], 60 | }, 61 | resolve: { 62 | extensions: ["*", ".js", ".jsx"], 63 | symlinks: false, 64 | }, 65 | output: { 66 | path: dir_build, 67 | publicPath: "/", 68 | filename: "bundle.js", 69 | clean: true, 70 | }, 71 | plugins: [ 72 | new CopyWebpackPlugin({ 73 | patterns: [{ from: dir_html }], 74 | }), 75 | new MiniCssExtractPlugin(), 76 | ], 77 | }; 78 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/index.js: -------------------------------------------------------------------------------- 1 | import { RipeAppWebComponents } from "@technical-design/ripe-app-webcomponents"; 2 | import { default as appNavBarMenu } from "./menu.json"; 3 | import { initRouter, routeTo, popStateRouter } from "./router"; 4 | 5 | // All css files imported here, 6 | // will be concatenated into a file `main.css` 7 | // by the webpack configs. 8 | // Note that main.css is the only filename that's tolerated by nginx. 9 | import "./styles/styles.css"; 10 | import "../node_modules/@technical-design/ripe-app-webcomponents/src/webcomponents/shared/ripe-app-colors.css"; 11 | 12 | 13 | window.addEventListener("onpopstate", function(e) { 14 | e.preventDefault(); 15 | console.log("DOM not ready yet. Refusing to navigate."); 16 | }); 17 | 18 | const navBarOpenCookieName = "ripencc-workbench-navbar-open"; 19 | const readCookie = name => { 20 | return (document.cookie.match(`(^|; )${name}=([^;]*)`) || 0)[2]; 21 | }; 22 | const setCookie = (name, val) => { 23 | document.cookie = `${name}=${val}; path=/`; 24 | }; 25 | 26 | const navbarCookie = readCookie(navBarOpenCookieName); 27 | 28 | const mobileUserAgentRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i; 29 | 30 | window.addEventListener("DOMContentLoaded", e => { 31 | const header = document.querySelector("ripe-header"); 32 | const navBar = document.querySelector("app-nav-bar"); 33 | 34 | if (navbarCookie === undefined || navbarCookie === "true") { 35 | // always start with closed menu on mobile devices 36 | if(mobileUserAgentRegex.test(navigator.userAgent)) { 37 | header.open = false; 38 | navBar.open = false; 39 | } else { 40 | header.open = true; 41 | navBar.open = true; 42 | } 43 | } else { 44 | header.open = false; 45 | navBar.open = false; 46 | } 47 | 48 | navBar.menu = JSON.stringify(appNavBarMenu); 49 | header.addEventListener("ripe-header-menu-toggle", e => { 50 | navBar.open = e.detail.open; 51 | setCookie(navBarOpenCookieName, e.detail.open ? "true" : "false"); 52 | }); 53 | navBar.addEventListener("app-nav-bar-select", e => { 54 | routeTo(e.detail.selected.url); 55 | }); 56 | window.onpopstate = popStateRouter; 57 | fetch('/version.txt') 58 | .then(response => response.text()) 59 | .then(text => { 60 | document.querySelector("#release-string").textContent = `Lab version ${text}`; 61 | }); 62 | 63 | initRouter(); 64 | }); 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Vagrant-based personal network lab - IPv6 Security 2 | ================================================== 3 | 4 | This is a small self contained lab for playing with IPv6 security tools. It uses [VirtualBox](https://www.virtualbox.org/) to run a virtual 5 | Linux server hosting a few [LXD](https://linuxcontainers.org/lxd/) containers 6 | running Ubuntu Linux. For a convenient access to 7 | the consoles of the virtual routers, [ttyd](https://github.com/tsl0922/ttyd) is 8 | used to provide web-based terminal access. 9 | 10 | The lab is highly customizable using [Vagrant](https://www.vagrantup.com/) to 11 | provide the virtual server and [Ansible](https://www.ansible.com/) to do the 12 | configuration. It should work on Windows, macOS and Linux. 13 | 14 | Installation of the pre-packaged release (recommended) 15 | ------------------------------------------------------ 16 | 17 | 1. Install [VirtualBox](https://www.virtualbox.org/) 18 | 2. Install [Vagrant](https://www.vagrantup.com/) 19 | 3. Create an empty folder (for instance `~/ripencc/ipv6seclab`) 20 | 4. Open a terminal window, enter the empty directory (using `cd` command) directory and run `vagrant init ripencc/ipv6seclab` followed by `vagrant up` 21 | 5. Wait a few minutes until vagrant finishes downloading the VM image. The 22 | size of the image is approximatelly 2 GiB. 23 | 6. Access the lab environment by pointing your web browser to [`http://localhost:8080/`](http://localhost:8080/) 24 | 25 | Stopping, restarting and destroying the lab 26 | ------------------------------------------- 27 | 28 | Then, you can turn off the VM by running `vagrant halt` in the same directory 29 | you run `vagrant up` before. You can use the latter command to restart the lab 30 | later. 31 | 32 | You can destroy the lab environment by issuing `vagrant destroy`. A subsequent 33 | call of `vagrant up` will bring up a completely fresh environment. 34 | 35 | Upgrading to a new version of the lab 36 | ------------------------------------- 37 | 38 | From time to time, a new version of the lab is released. You can spot it by 39 | the contents of `version.txt` file. If you want to upgrade, run these commands: 40 | 41 | vagrant destroy 42 | vagrant box update 43 | vagrant up 44 | 45 | Resolving port conflict 46 | ----------------------- 47 | 48 | In case there is some other software listening on local port 8080, Vagrant will refuse 49 | to start. You can either stop the offending software or adjust Vagrantfile to change 50 | the port number. For example, this is how you move the environment from port 8080 to 51 | port 8081: 52 | 53 | ```ruby 54 | config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1", id: "web", disabled: true 55 | config.vm.network "forwarded_port", guest: 80, host: 8081, host_ip: "127.0.0.1" 56 | ``` 57 | 58 | Then you can find the lab environment on [`http://localhost:8081/`](http://localhost:8081/) 59 | 60 | Running the development version 61 | ------------------------------- 62 | 63 | You can also clone or download this repository and run `vagrant up` there. This 64 | should build a completely new environment from scratch. 65 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/main.css: -------------------------------------------------------------------------------- 1 | /*!*************************************************************************!*\ 2 | !*** css ./node_modules/css-loader/dist/cjs.js!./app/styles/styles.css ***! 3 | \*************************************************************************/:root{--ripe-app-font-family:"Public Sans"}@media only screen and (max-width:700px){#app-grid{grid-template-columns:[left] 300px [right] auto}#app-banners-container{top:40px}#app-banners-container,#app-workspace{grid-column:left/span 2}}@media only screen and (min-width:701px){#app-grid{grid-template-columns:[left] max-content [right] auto}#app-banners-container{grid-column:right;top:70px}#app-workspace{grid-column:right}}@media only screen and (max-width:400px){#atlas-logo-title{max-width:100px}}#app-grid{background-color:#fafafa;display:grid;grid-template-rows:[bc] min-content [ws] auto;width:100%}app-nav-bar{grid-column:left;grid-row:bc/span 1000}app-switcher{font-weight:400}#app-banners-container{background-color:#fff;grid-row:bc;position:sticky}#app-workspace{background-color:var(--ripe-app-main-background,#fafafa);grid-row:ws;margin:0}#csr-workspace{padding:24px}body,html{box-sizing:border-box;font-family:var(--ripe-app-font-family),sans-serif;font-size:16px;line-height:1.5rem;margin:0;width:100%}#fb div{display:inline-block}.accent{color:var(--ripe-app-positive)}.alarmed{color:var(--ripe-app-alarmed)}roll-over a:visited{color:var(--ripe-app-secondary-20)}roll-over a{color:var(--ripe-app-secondat-100)}roll-over p{line-height:1.4rem} 4 | 5 | /*!******************************************************************************************************************************************************!*\ 6 | !*** css ./node_modules/css-loader/dist/cjs.js!./node_modules/@technical-design/ripe-app-webcomponents/src/webcomponents/shared/ripe-app-colors.css ***! 7 | \******************************************************************************************************************************************************/:root{--ripe-app-accent:#25316a;--ripe-app-darkbase:#070033;--ripe-app-backgrounds:var(--ripe-app-primary-100);--ripe-app-info:#29abe2;--ripe-app-positive:#6ebe49;--ripe-app-worried:#ff6a00;--ripe-app-alarmed:#ed1c24;--ripe-app-accent:var(--ripe-app-primary-80);--ripe-app-primary-100:#080037;--ripe-app-primary-80:#1b204c;--ripe-app-primary-60:#2f376b;--ripe-app-primary-40:#9ea5c2;--ripe-app-primary-20:#d7dce8;--ripe-app-primary-0:#eceff4;--ripe-app-secondary-100:#009ba6;--ripe-app-secondary-80:#00b0ba;--ripe-app-secondary-60:#3dbdc6;--ripe-app-secondary-40:#7bd1db;--ripe-app-secondary-20:#c2e8ed;--ripe-app-secondary-0:#e1eff4;--ripe-app-positive-100:#6ebe49;--ripe-app-positive-80:#7cce5d;--ripe-app-positive-60:#9bd78b;--ripe-app-positive-40:#c4e4bc;--ripe-app-positive-20:#ddeddc;--ripe-app-positive-0:#edf3ed;--ripe-app-worried-100:#ff6a00;--ripe-app-worried-80:#ff7a36;--ripe-app-worried-60:#ff9660;--ripe-app-worried-40:#ffb491;--ripe-app-worried-20:#ffdacc;--ripe-app-worried-0:#ffeae7;--ripe-app-navbar-background:var(--ripe-app-primary-100);--ripe-app-navbar-menuitem:var(--ripe-app-primary-20);--ripe-app-navbar-active-menuitem:#fff;--ripe-app-navbar-footer-item:#9d9aac;--ripe-app-navbar-indicator:#f59331;--ripe-app-navrail-tooltip-bg:#363738;--ripe-app-navrail-tooltip-text:#fff;--ripe-app-navrail-tooltip-border:#363738;--ripe-app-header-icon-bg:silver;--ripe-app-header-icon-hover:var(--ripe-app-primary-100);--ripe-app-border-line:#2f2c56;--ripe-header-padding-top:0px} -------------------------------------------------------------------------------- /ubuntu-auto-install/README.md: -------------------------------------------------------------------------------- 1 | Autoinstall files for Ubuntu 2 | ============================ 3 | 4 | The contents of this directory can be used to automate fresh installation 5 | of the lab environment without the help of Vagrant or Virtualbox. It uses 6 | [Automated Server 7 | installation](https://ubuntu.com/server/docs/install/autoinstall) feature 8 | of Ubuntu server together with [cloud-init](https://cloud-init.io/). It can 9 | run anywhere where Ubuntu 22.04 is supported, including ARM architecture 10 | computers like M1/M2 Apple Hardware. It has been tested using 11 | [UTM](https://mac.getutm.app/) hypervisor on both arm64 and x86_64 12 | architectures. Other hypervisors and/or architectures might or might not 13 | work. 14 | 15 | Prerequisites for building UTM-based Virtual Machine 16 | ---------------------------------------------------- 17 | - A computer running macOS based on either Intel or Apple Silicon 18 | - At least 10 GB of free disk space 19 | - [UTM](https://mac.getutm.app/) 20 | - Ubuntu Server ISO file for particular architecture: 21 | [Intel](https://ubuntu.com/download/server) or 22 | [ARM](https://ubuntu.com/download/server/arm) 23 | 24 | Preparing virtual machine from scratch 25 | -------------------------------------- 26 | 27 | 1. In UTM, press "Create a New Virtual Machine." 28 | 2. Select "Virtualize", then "Linux". Provide ISO image. Provision at least 2048 29 | GB RAM and 10 GB disk. The rest can stay on default. 30 | 3. Edit machine settings, remove sound card by right clicking it and edit QEMU 31 | options providing path to Autoinstall files. At the very bottom, in the field 32 | with prompt "New…", put this path: 33 | 34 | ``` 35 | -smbios type=1,serial=ds=nocloud-net;s=https://raw.githubusercontent.com/RIPE-NCC/ipv6-security-lab/main/ubuntu-auto-install/ 36 | ``` 37 | 4. In case of Intel computer, adjust virtual machine setup like this: 38 | - Display: Emulated Display Card: `virtio-vga-gl` 39 | - Network: Emulated Network Card: `virtio-net-pci` 40 | - Hard disk: Interface: `VirtIO` 41 | 42 | 5. Run the VM. It should start booting from the ISO image and spit a lot of 43 | text. 44 | 6. You will be prompted "Continue with autoinstall? (yes/no)". Answer `yes`. 45 | 7. After several minutes, the installer will reboot the virtual machine. Do NOT 46 | boot the ISO image again, eject it or select "Boot from next volume." 47 | 8. During the first start, the installation of the lab environment takes place. 48 | Please wait at least minutes. You can log in using password "ubuntu" or 49 | just wait. Don't get bothered by "Login timed out after 60 seconds." 50 | 9. After installation is done, you will be logged in automatically. The last 51 | line before prompt should read: "status: done." 52 | 53 | The lab environment should be fully accessible on a random IP address the 54 | Virtual Machine got assigned. You can see the IP address by using command 55 | `ip -br addr show enp0s1`. You can stop the VM gracefully by issuing `poweroff`. 56 | 57 | Final touches 58 | ------------- 59 | 60 | After the installation is done, you can prepare a VM image that would be 61 | suitable for distribution. In this, we change network setup so it uses localhost 62 | port 8080 and we remove display so the VM can run headless. 63 | 64 | 1. Shut down the image gracefully by issuing `poweroff` in the console or `sudo 65 | poweroff` in the admin console of the web interface. 66 | 2. In VM settings, change Network Mode to "Emulated VLAN". A new section called 67 | "Port Forwarding" appears in the menu. 68 | 3. In the "Port Forwarding" menu, add a new item and fill in: Protocol TCP, 69 | `10.0.2.15`, `80`, `127.0.0.1`, `8080`. 70 | 4. Remove display device 71 | 5. In VirtIO Drive, press Compress. This will significantly reduce the disk 72 | image. 73 | 6. Add descriptive name and put version as well as architecture into Notes. 74 | 7. Press Share button to share UTM image. Since the image is actually a folder, 75 | compress it with zip before uploading it somewhere. 76 | 77 | Troubleshooting 78 | --------------- 79 | 80 | - From time to time, automated OS installation (the first step) fails for no 81 | obvious reason. Resetting the VM and running it again usually helps. 82 | - The Ansible provisioning part can get stuck many ways since it downloads data 83 | from several sources around the Internet. You can see the logs only after it 84 | fails in `/var/log/cloud-init.log`. You can also try to run Ansible manually 85 | to see the exact cause of errors/delays : 86 | ``` 87 | $ ansible-playbook -i ipv6-security-lab/ansible/inventory.ini ipv6-security-lab/ansible/site.yaml 88 | ``` 89 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.20240816.0](https://www.github.com/RIPE-NCC/ipv6-security-lab/compare/v0.20230403.0...v0.20240816.0) (2024-08-16) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * Add warning about RIPE NCC internal dependency of the frontend ([3ab20e3](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/3ab20e3088ab23d1d233f535d645da75a26f96b2)) 9 | * fix Debian Stretch repo location ([f42f93d](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/f42f93d4661e7d40b30b2604374b3ecc2e9decd0)) 10 | * restore kernel-level IPv6 workaround ([b816308](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/b816308a71ef2ba56e194e877b749d1411e56f98)) 11 | * switch to official Ubuntu images ([5b152de](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/5b152de185cea89735d313d8231443b2e5eedbc4)) 12 | * update npm dependencies ([85e48f5](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/85e48f5cc0d4956d8c4089d703907fdb931eb8bc)) 13 | 14 | ## [0.20230403.0](https://www.github.com/RIPE-NCC/ipv6-security-lab/compare/v0.20221114.0...v0.20230403.0) (2023-04-03) 15 | 16 | 17 | ### Features 18 | 19 | * support Ubuntu auto-install, upgrade host to Ubuntu 22.04 ([b34e011](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/b34e0116adf75bf18aada1e71f320d6c0884d69c)) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * remove obsolete Ansible syntax ([ddb8601](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/ddb86017a5e66b0b4c679dac4526cb51af2cb440)) 25 | 26 | 27 | ### Miscellaneous Chores 28 | 29 | * release 0.20230403.0 ([c13aad6](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/c13aad6162ae6713fdf7d5d4fdd90ab4514fa4c9)) 30 | 31 | ## [0.20221114.0](https://www.github.com/RIPE-NCC/ipv6-security-lab/compare/v0.20211020.1...v0.20221114.0) (2022-11-14) 32 | 33 | 34 | ### Features 35 | 36 | * frontend update ([d4464f5](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/d4464f5b5e61474efb9225c033d52d7175ff8f73)) 37 | * Upgrade ttyd to 1.7.2 ([28991b9](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/28991b91f42ec52b87479abe1d2bffeb1b302a2d)) 38 | 39 | ### [0.20211020.1](https://www.github.com/RIPE-NCC/ipv6-security-lab/compare/v0.20211020.0...v0.20211020.1) (2021-10-20) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * use macos-10.15 in GitHub action ([741758c](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/741758cc50ea57b23fbd97d26f78e7c7988418cb)) 45 | 46 | ## [0.20211020.0](https://www.github.com/RIPE-NCC/ipv6-security-lab/compare/v0.20210814.0...v0.20211020.0) (2021-10-20) 47 | 48 | 49 | ### Features 50 | 51 | * update Termshark to 2.3.0 ([5026edc](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/5026edc61a88618dca4a72c718e22bd21ddccd92)) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * ansible-base got renamed to ansible-core ([b7f39c0](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/b7f39c0a90c4f0ec29b1d17c291c38cb83e73d07)) 57 | * update web frontend to the latest version ([6c213b9](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/6c213b9214a017442f49353adc1a06aa603a3bdb)) 58 | * Use rsync instead of shared folders ([ee1de25](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/ee1de255f5780d974aa88b59f2293855014c6e65)) 59 | 60 | ## [0.20210814.0](https://www.github.com/RIPE-NCC/ipv6-security-lab/compare/v0.20210813.0...v0.20210814.0) (2021-08-17) 61 | 62 | 63 | ### Features 64 | 65 | * Disable multicast snooping on lxdbr0 by default ([2580b2c](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/2580b2c63d704a147e5dcc7c1a54bebe9040b9ae)) 66 | * frontend update ([c47e88e](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/c47e88eeacd48ee296ee9ab67a61aea1c87e59ad)) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * disable shared folder, workaround serial port issue on Windows ([5938407](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/5938407dcd8921839591ce9ae16018cb160d53ff)) 72 | 73 | ## [0.20210813.0](https://www.github.com/RIPE-NCC/ipv6-security-lab/compare/v0.20210709.0...v0.20210813.0) (2021-08-13) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * ensure exact version of base box ([972b4e1](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/972b4e1125cf4566d37b71e51d02bd81b05d41c0)) 79 | * workaround ignored ICMPv6 redirects ([60313d1](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/60313d14532f414794289dcbcee06caf3be99f60)) 80 | 81 | ## 0.20210709.0 (2021-07-09) 82 | 83 | 84 | ### Features 85 | 86 | * add Vagrantfile-in-box to be part of the box ([526e619](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/526e619ac00eb774e438eef8bc8865b6f4b3fe89)) 87 | * fixed MAC address of the virtual router ([520f09a](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/520f09a4fc51470254a39873069b03dc9190bc14)) 88 | * replace the frontend with a RIPE NCC styled one ([a10183f](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/a10183f9f388a4b1690a015f0e79cdf7dae48760)) 89 | * switch to ubuntu/focal64 base box ([5fa8f70](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/5fa8f70003cbe54eefdb142833dbb66bbcfa1615)) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * frontend: small adjustments ([bb221aa](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/bb221aa61c429c2f95e869259449234ebb5f73fb)) 95 | * frontend: Workaround broken iframe resize on Firefox ([0cbdbc6](https://www.github.com/RIPE-NCC/ipv6-security-lab/commit/0cbdbc61ae4afac4212f4c0c9d01310d95502218)) 96 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": [ 3 | { 4 | "title": "Dashboard", 5 | "subtitle": "Access to the servers' consoles", 6 | "url": "/", 7 | "icon": "" 8 | }, 9 | { 10 | "title": "Network diagram", 11 | "subtitle": "How are the machines connected", 12 | "url": "/network/", 13 | "icon": "" 14 | } 15 | ], 16 | "footer": [ 17 | { 18 | "title": "Admin console", 19 | "subtitle": "Control the VM itself", 20 | "url": "/admin/", 21 | "icon": "" 22 | }, 23 | { 24 | "title": "Legal", 25 | "url": "/legal/", 26 | "subtitle": "Copyright, Privacy, Terms and Cookies", 27 | "icon": "" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/router.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { default as appNavBarMenu } from "./menu.json"; 4 | 5 | export const initRouter = (reregister = false) => { 6 | console.log( 7 | `Router: init router ${ 8 | (reregister && "(reregister)") || "" 9 | }` 10 | ); 11 | 12 | // initial client-side url to iframe routing 13 | if (!reregister) { 14 | routeTo( 15 | `${document.location.pathname}${document.location.search}${document.location.hash}`, 16 | { initial: true, pushState: false } 17 | ); 18 | } 19 | }; 20 | 21 | export const popStateRouter = function (e) { 22 | e.preventDefault(); 23 | const pathname = document.location.pathname; 24 | const search = document.location.search; 25 | const hash = document.location.hash; 26 | console.log(`Router: User pops to ${pathname}${search}${hash}`); 27 | console.log(`or ${document.location.href}`); 28 | 29 | routeTo(pathname, { 30 | pushState: false, 31 | search: search, 32 | hash: hash, 33 | forceReload: true, 34 | }); 35 | }; 36 | 37 | export const setActiveMenuItemByUrl = (newPathName) => { 38 | // Search for the url in the menubar and make the item active if found, 39 | // excluding the search params and hash from the new url and 40 | // excluding any integers (measurement or probe ids) 41 | const menuItems = appNavBarMenu.main.concat(appNavBarMenu.footer); 42 | const matchPath = newPathName 43 | .replace(/(\?.+)/, "") 44 | .replace(/(\#.+)/, ""); 45 | console.log(`Router: matching URL ${matchPath}`); 46 | const newActiveItem = menuItems.find( 47 | (i) => 48 | i.url === (matchPath || matchPath.replace(/^\/([^\/]+)\/(.+)/, "/$1/")) 49 | ); 50 | if (newActiveItem) { 51 | console.log(`Router: found menu item ${newActiveItem.title}`); 52 | document.querySelector("app-nav-bar").active = newActiveItem.title; 53 | } else { 54 | console.log(`Router: no menu item found for ${matchPath}`); 55 | } 56 | return newActiveItem; 57 | }; 58 | 59 | export const routeTo = (newUrl, options = {}) => { 60 | /* 61 | * Arguments: (URL, OPTIONS) 62 | * 63 | * URL 64 | * 65 | * OPTIONS 66 | * 67 | * pushState : if set to false, do not change the URL in the addressbar. 68 | * inital : if set to true load the iframe, even if it's already in the addressbar. 69 | * forceReload: if set to true will *always* reload the frame, 70 | * even if nothing of the three-tuple pathname, search * hash changed. 71 | * search : search parameters for newUrl 72 | * hash : hash parameter for newUrl 73 | */ 74 | 75 | //newUrl = newUrl.replace(/(.+)\/$/, "$1"); 76 | 77 | const currentCsrWorkSpace = document.querySelector("div#csr-workspace"); 78 | const currentUrlPath = document.location.pathanme; 79 | const currentSearch = document.location.search; 80 | const currentHash = document.location.hash; 81 | 82 | updateTitle(newUrl); 83 | if ( 84 | !options.initial && 85 | !options.forceReload && 86 | `${newUrl}${options.search || ""}${options.hash || ""}` === 87 | `${currentUrlPath}${currentSearch}${currentHash}` 88 | ) { 89 | console.log( 90 | `Router: Nothing changed. Staying at ${currentUrlPath}${currentSearch}${currentHash}` 91 | ); 92 | return; 93 | } 94 | 95 | let optionsPushState = options && options.pushState; 96 | if (typeof optionsPushState === "undefined") { 97 | optionsPushState = true; 98 | } 99 | console.log(`Router: change pushState=${optionsPushState}`); 100 | 101 | // OTOH only the hashpart or the search parameters 102 | // might have changed. So update only the URL if that's the case 103 | if ( 104 | !options.initial && 105 | !options.forceReload && 106 | (options.hash || options.search) && 107 | currentUrlPath === newUrl 108 | ) { 109 | if (optionsPushState && (options.hash || options.search)) { 110 | console.log("History: Push history"); 111 | window.history.pushState( 112 | { 113 | changeFrame: false, 114 | fromUrl: window.location.pathname, 115 | hash: window.location.hash, 116 | search: window.location.search, 117 | }, 118 | "", 119 | `${newUrl}${(options.search && options.search) || ""}${ 120 | (options.hash && options.hash) || "" 121 | }` 122 | ); 123 | } 124 | 125 | console.log( 126 | `Router: Changed search parms and/or hash to ${options.search}${options.hash}.` 127 | ); 128 | 129 | return; 130 | } 131 | 132 | // Go over the list of client-side rendered pages in /workspaces. 133 | // If the URL is in there, we invoke the function that handles this route 134 | // client-side 135 | const cleanUrl = newUrl.replace(/\//g, "").replace(/^$/, "dashboard"); 136 | console.log(`Router: routing to: ${cleanUrl}`); 137 | fetchWorkSpace(cleanUrl).then((m) => { 138 | m.loadWorkSpace(); 139 | console.log( 140 | `Router: Push state ${newUrl}` 141 | ); 142 | window.history.pushState( 143 | {}, 144 | "", 145 | `${newUrl}` 146 | ); 147 | }, (m) => { 148 | console.log(`Router: Workspace not found`); 149 | fetchWorkSpace("404").then((m) => { 150 | m.loadWorkSpace(); 151 | }); 152 | } 153 | ); 154 | currentCsrWorkSpace.style.display = "block"; 155 | setActiveMenuItemByUrl(newUrl); 156 | }; 157 | 158 | // update the document title to something more specific 159 | // based on the new URL of the loaded frame 160 | const updateTitle = newUrl => { 161 | if (newUrl.slice(-1) !== "/") { 162 | newUrl = `${newUrl}/`; 163 | } 164 | // find matching title 165 | const menuItems = appNavBarMenu.main.concat(appNavBarMenu.footer); 166 | // not all URLs have trailing slash in menuItems 167 | // have to add them here for easier match comparison 168 | const titleItems = menuItems.map(i => { 169 | return { 170 | url: i.url.slice(-1) === "/" ? i.url : `${i.url}/`, 171 | title: i.title, 172 | }; 173 | }); 174 | const matchItem = titleItems.find(i => i.url === newUrl); 175 | 176 | // if we find a matching side menu entry, use that title 177 | if (matchItem) { 178 | document.title = `${matchItem.title} | RIPE NCC Workbench`; 179 | } 180 | } 181 | 182 | const fetchWorkSpace = async (path) => { 183 | let wsHandler = await import(`./workspaces/${path}/index.js`); 184 | return wsHandler; 185 | }; 186 | 187 | -------------------------------------------------------------------------------- /ripe-app-frontend/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | RIPE NCC Workbench 15 | 16 | 17 | 18 | 19 | 20 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 50 | 51 | 54 | 55 | 56 | 59 | 62 | 64 | 67 | 71 | 74 | 77 | 80 | 82 | 83 | 84 | 85 | 86 |

About RIPE NCC Workbench

87 |

88 |
89 |
90 |
91 | 92 |
93 | 94 |
95 |
96 |
97 |
98 | 99 | 100 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | RIPE NCC Workbench 15 | 16 | 17 | 18 | 19 | 20 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 50 | 51 | 54 | 55 | 56 | 59 | 62 | 64 | 67 | 71 | 74 | 77 | 80 | 82 | 83 | 84 | 85 | 86 |

About RIPE NCC Workbench

87 |

88 |
89 |
90 |
91 | 92 |
93 | 94 |
95 |
96 |
97 |
98 | 99 | 100 | -------------------------------------------------------------------------------- /ansible/site.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Deploy LXD 4 | hosts: default 5 | become: false 6 | tags: container 7 | handlers: 8 | - name: reboot hostA 9 | delegate_to: hostA 10 | reboot: 11 | 12 | - name: reload udev rules 13 | command: udevadm control --reload-rules 14 | become: true 15 | 16 | tasks: 17 | - name: Disable lxdbr0 multicast snooping 18 | become: true 19 | copy: 20 | dest: /etc/udev/rules.d/99-disable-lxdbr0-mcast-snooping.rules 21 | content: SUBSYSTEM=="net", ACTION=="add", KERNEL=="lxdbr0", RUN+="/bin/sh -c 'echo 0 > /sys/class/net/lxdbr0/bridge/multicast_snooping'" 22 | notify: reload udev rules 23 | 24 | - meta: flush_handlers 25 | 26 | - name: Initialize LXD 27 | command: 28 | cmd: lxd init --preseed 29 | stdin: "{{ lookup('file', 'lxd_preseed.yaml') }}" 30 | creates: /var/snap/lxd/common/state 31 | 32 | - name: Create first container 33 | lxd_container: 34 | name: hostA 35 | state: started 36 | config: 37 | volatile.eth0.hwaddr: "00:16:3e:ee:00:0a" 38 | source: 39 | type: image 40 | mode: pull 41 | server: https://cloud-images.ubuntu.com/releases/ 42 | protocol: simplestreams 43 | alias: focal 44 | profiles: ["default"] 45 | wait_for_ipv4_addresses: true 46 | 47 | - name: Disable systemd-networkd IPv6 handling (workaround broken redirects) 48 | delegate_to: hostA 49 | lineinfile: 50 | path: /etc/netplan/50-cloud-init.yaml 51 | insertafter: "^ dhcp4: true" 52 | line: " accept-ra: false" 53 | notify: reboot hostA 54 | 55 | - name: Enable kernel-level IPv6 autoconf (workaround broken redirects) 56 | delegate_to: hostA 57 | copy: 58 | dest: /etc/sysctl.d/99-accept_ra.conf 59 | content: "net.ipv6.conf.eth0.accept_ra = 1" 60 | notify: reboot hostA 61 | 62 | - name: Hack enable kernel-level IPv6 autoconf (workaround broken redirects) 63 | delegate_to: hostA 64 | copy: 65 | dest: /etc/rc.local 66 | mode: u=rwx,g=rx,o=rx 67 | content: '#!/bin/sh 68 | 69 | sysctl -w net.ipv6.conf.eth0.accept_ra=1 70 | 71 | ' 72 | notify: reboot hostA 73 | 74 | - meta: flush_handlers 75 | 76 | - name: Install python3-apt 77 | delegate_to: hostA 78 | command: 79 | cmd: apt-get -y install python3-apt 80 | creates: /usr/lib/python3/dist-packages/apt/package.py 81 | 82 | - name: Install necessary packages 83 | delegate_to: hostA 84 | package: 85 | update_cache: true 86 | name: 87 | - python3-pip 88 | - python3 89 | - tcpdump 90 | - wget 91 | - libpcap-dev 92 | - libssl-dev 93 | - gcc 94 | - python3-setuptools 95 | - git 96 | - make 97 | - man 98 | - nftables 99 | - iptables 100 | - nmap 101 | - tshark 102 | - nano 103 | - joe 104 | - vim 105 | 106 | - name: Upgrade pip3 107 | delegate_to: hostA 108 | pip: 109 | executable: pip3 110 | state: latest 111 | name: pip 112 | 113 | - name: Install python packages 114 | delegate_to: hostA 115 | pip: 116 | executable: pip3 117 | state: latest 118 | name: 119 | - scapy[basic] 120 | - cryptography 121 | - netaddr 122 | 123 | - name: Clone IPv6 toolkit repository 124 | delegate_to: hostA 125 | git: 126 | repo: https://github.com/fgont/ipv6toolkit 127 | dest: /usr/src/ipv6toolkit 128 | register: clone_toolkit 129 | 130 | - name: Compile the IPv6 toolkit 131 | delegate_to: hostA 132 | command: 133 | cmd: make all 134 | chdir: /usr/src/ipv6toolkit 135 | when: clone_toolkit.changed 136 | register: compile_toolkit 137 | 138 | - name: Install the IPv6 toolkit 139 | delegate_to: hostA 140 | command: 141 | cmd: make install 142 | chdir: /usr/src/ipv6toolkit 143 | when: compile_toolkit.changed 144 | 145 | - name: Clone the THC-IPv6 repository 146 | delegate_to: hostA 147 | git: 148 | repo: https://github.com/vanhauser-thc/thc-ipv6 149 | dest: /usr/src/thc-ipv6 150 | register: clone_thc 151 | 152 | - name: Compile the THC-IPv6 toolkit 153 | delegate_to: hostA 154 | command: 155 | cmd: make 156 | chdir: /usr/src/thc-ipv6 157 | when: clone_thc.changed 158 | register: compile_thc 159 | 160 | - name: Install the THC-IPv6 toolkit 161 | delegate_to: hostA 162 | command: 163 | cmd: make install 164 | chdir: /usr/src/thc-ipv6 165 | when: compile_thc.changed 166 | 167 | - name: Download Termshark x64 168 | delegate_to: hostA 169 | when: ansible_architecture == "x86_64" 170 | unarchive: 171 | src: https://github.com/gcla/termshark/releases/download/v2.4.0/termshark_2.4.0_linux_x64.tar.gz 172 | remote_src: yes 173 | dest: /usr/local/bin/ 174 | extra_opts: 175 | - --transform 176 | - "s_^.*/__" 177 | 178 | - name: Download Termshark arm64 179 | delegate_to: hostA 180 | when: ansible_architecture == "aarch64" 181 | unarchive: 182 | src: https://github.com/gcla/termshark/releases/download/v2.4.0/termshark_2.4.0_linux_arm64.tar.gz 183 | remote_src: yes 184 | dest: /usr/local/bin/ 185 | extra_opts: 186 | - --transform 187 | - "s_^.*/__" 188 | 189 | - name: Clone container 190 | lxd_container: 191 | name: "{{ item.name }}" 192 | config: 193 | volatile.eth0.hwaddr: "{{ item.mac }}" 194 | state: started 195 | source: 196 | type: copy 197 | source: hostA 198 | profiles: ["default"] 199 | wait_for_ipv4_addresses: true 200 | with_items: 201 | - name: hostB 202 | mac: 00:16:3e:ee:00:0b 203 | - name: hostC 204 | mac: 00:16:3e:ee:00:0c 205 | 206 | - name: Host provision 207 | hosts: default 208 | become: true 209 | tags: host 210 | tasks: 211 | - name: Install python3-apt 212 | command: 213 | cmd: apt-get -y install python3-apt 214 | creates: /usr/lib/python3/dist-packages/apt/package.py 215 | 216 | - name: Import Debian Stretch repo key (for tmux 2.3) 217 | apt_key: 218 | url: "https://ftp-master.debian.org/keys/archive-key-9.asc" 219 | 220 | - name: Enable Debian Stretch repo (for tmux 2.3) 221 | apt_repository: 222 | repo: deb http://archive.debian.org/debian stretch main 223 | 224 | - name: Check tmux version 225 | command: tmux -V 226 | changed_when: false 227 | ignore_errors: true 228 | register: tmux_v 229 | 230 | - name: Uninstall newer tmux 231 | apt: 232 | package: tmux 233 | state: absent 234 | autoremove: yes 235 | when: tmux_v.stdout != "tmux 2.3" 236 | ignore_errors: true 237 | 238 | - name: Install tmux dependency 239 | apt: 240 | package: libtinfo5 241 | 242 | - name: Install tmux from Debian Stretch 243 | apt: 244 | package: tmux 245 | default_release: stretch 246 | 247 | - name: Install necessary packages 248 | apt: 249 | package: 250 | - nginx 251 | - libwebsockets8 252 | 253 | - name: Setup WebSockets connection upgrade 254 | copy: 255 | src: nginx/connection_upgrade.conf 256 | dest: "/etc/nginx/conf.d/" 257 | mode: 0644 258 | notify: reload nginx 259 | 260 | - name: Disable absolute redirects 261 | lineinfile: 262 | line: "absolute_redirect off;" 263 | dest: "/etc/nginx/conf.d/no_absolute_redirects.conf" 264 | create: yes 265 | mode: 0644 266 | notify: reload nginx 267 | 268 | - name: Setup ttyd proxy snippet 269 | copy: 270 | src: nginx/ttydconsole.conf 271 | dest: "/etc/nginx/snippets/" 272 | mode: 0644 273 | notify: reload nginx 274 | 275 | - name: Enable ttyd proxy snippet 276 | lineinfile: 277 | path: "/etc/nginx/sites-available/default" 278 | line: "include /etc/nginx/snippets/ttydconsole.conf;" 279 | insertbefore: "^\\s*location / {.*$" 280 | notify: reload nginx 281 | 282 | - name: Deploy static website 283 | copy: 284 | src: ../../ripe-app-frontend/build/ 285 | dest: /var/www/html/ 286 | owner: www-data 287 | group: www-data 288 | mode: 0644 289 | 290 | - name: Deploy version file 291 | copy: 292 | src: ../../version.txt 293 | dest: /var/www/html/ 294 | owner: www-data 295 | group: www-data 296 | mode: 0644 297 | 298 | - name: Route everything to index.html page 299 | lineinfile: 300 | path: "/etc/nginx/sites-available/default" 301 | line: " try_files $uri /index.html;" 302 | regexp: '^\s*try_files \$uri' 303 | notify: reload nginx 304 | 305 | - name: Start Nginx 306 | service: 307 | name: nginx 308 | state: started 309 | enabled: on 310 | 311 | - name: Download ttyd 312 | get_url: 313 | url: "https://github.com/tsl0922/ttyd/releases/download/1.7.3/ttyd.{{ ansible_architecture }}" 314 | dest: /opt/ttyd 315 | mode: 0755 316 | 317 | - name: Deploy tmux config file 318 | copy: 319 | src: tmux.conf 320 | dest: "/etc/" 321 | mode: 0644 322 | 323 | - name: Deploy ttyd-container unit files 324 | template: 325 | src: "systemd/{{ item }}" 326 | dest: "/etc/systemd/system/" 327 | mode: 0644 328 | notify: systemctl daemon-reload 329 | with_items: 330 | - ttyd-container@.service 331 | - ttyd-admin.service 332 | - ttyd-container.target 333 | 334 | - name: Deploy ttyd-container generator 335 | template: 336 | src: systemd/ttyd-container.generator 337 | dest: "/etc/systemd/system-generators/" 338 | mode: 0755 339 | notify: systemctl daemon-reload 340 | 341 | - name: Enable and start ttyd-container target 342 | service: 343 | name: ttyd-container.target 344 | state: started 345 | enabled: on 346 | 347 | - name: Enable and start ttyd-admin 348 | service: 349 | name: ttyd-admin.service 350 | state: started 351 | enabled: on 352 | 353 | - name: Trim unused Btrfs space for LXD 354 | command: nsenter -m/run/snapd/ns/lxd.mnt fstrim -av 355 | become: true 356 | changed_when: false 357 | 358 | - name: Trim unused host space 359 | command: fstrim -av 360 | become: true 361 | changed_when: false 362 | 363 | handlers: 364 | - name: reload nginx 365 | service: 366 | name: nginx 367 | state: reloaded 368 | 369 | - name: systemctl daemon-reload 370 | systemd: 371 | daemon_reload: yes 372 | 373 | 374 | ... 375 | -------------------------------------------------------------------------------- /ripe-app-frontend/build/38f376c724439f780378.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xmlhostAROUTERhostBhostC::1eth0eth0eth0Network Prex:2001:db8:f:1::/64RA::216:3e:feee:a::216:3e:feee:b::216:3e:feee:c 402 | -------------------------------------------------------------------------------- /ripe-app-frontend/app/workspaces/network/network_diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xmlhostAROUTERhostBhostC::1eth0eth0eth0Network Prex:2001:db8:f:1::/64RA::216:3e:feee:a::216:3e:feee:b::216:3e:feee:c 402 | --------------------------------------------------------------------------------