diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..5ff6309b7199129c1afe4f4ec1906e640bec48c6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..13566b81b018ad684f3a35fee301741b2734c8f4
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..aa00ffab7828f4818589659c804ec2cfd99baed3
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding">
+    <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
+    <file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4258c62fd9edaffc241e79c34ec36a478505f4f1
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="MavenProjectsManager">
+    <option name="originalFiles">
+      <list>
+        <option value="$PROJECT_DIR$/pom.xml" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_18" default="true" project-jdk-name="18" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..35eb1ddfbbc029bcab630581847471d7f238ec53
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0a62eb6da9ad9324c78dfac3a9ffaf50da07c899
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.internetworking</groupId>
+    <artifactId>InternetworkingWS23</artifactId>
+    <version>1.0-SNAPSHOT</version>
+
+    <properties>
+        <maven.compiler.source>18</maven.compiler.source>
+        <maven.compiler.target>18</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-junit-jupiter</artifactId>
+            <version>4.6.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+            <version>4.6.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>5.9.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.9.0</version>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/src/main/java/apps/CPClient.java b/src/main/java/apps/CPClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..6a1d55c5482bda8a72e082123648b9e86db9160c
--- /dev/null
+++ b/src/main/java/apps/CPClient.java
@@ -0,0 +1,62 @@
+package apps;
+
+import cp.*;
+import exceptions.*;
+import phy.*;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.InetAddress;
+
+public class CPClient {
+    private static final String SERVER_NAME = "localhost";
+
+
+    public static void main(String[] args) {
+        // Each client needs to start on a unique UDP port provided by the user
+        if (args.length != 1) {
+            System.out.println("Provide an address identifier (int) from range [5000:65534]");
+            return;
+        }
+        var id = Integer.parseInt(args[0]);
+        if (id < 5000 || id > 65534) {
+            System.out.println("Invalid address identifier! Range [5000:65534]");
+            return;
+        }
+
+        // Set up the virtual link protocol
+        PhyProtocol phy = new PhyProtocol(id);
+
+        // Set up command protocol
+        CPProtocol cp = null;
+        try {
+            cp = new CPProtocol(InetAddress.getByName(SERVER_NAME), CPServer.SERVER_PORT, phy);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        // Read data from user to send to server
+        BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
+        boolean eof = false;
+        while (!eof) {
+            try {
+                String sentence = null;
+                System.out.println("Command: ");
+                sentence = inFromUser.readLine();
+                // Currently only these two commands are supported by the specification
+                if(!(sentence.equals("status") || sentence.startsWith("print")))
+                    continue;
+
+                cp.send(sentence.trim(), null);
+                System.out.println("Command sent to server ... wating for response");
+                String answer = cp.receive().getData();
+                System.out.println(answer);
+            } catch (IllegalCommandException e) {
+                System.out.println("Only these two commands are supported: status, print \"text\"");
+            } catch (IWProtocolException | IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}
diff --git a/src/main/java/apps/CPServer.java b/src/main/java/apps/CPServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..071a8eddca62f9ce581a813fd05d20532db66703
--- /dev/null
+++ b/src/main/java/apps/CPServer.java
@@ -0,0 +1,37 @@
+package apps;
+
+import core.Msg;
+import cp.CPProtocol;
+import exceptions.IWProtocolException;
+import phy.PhyProtocol;
+
+import java.io.IOException;
+
+public class CPServer {
+    protected static final int SERVER_PORT = 3027;
+
+    public static void main(String[] args) {
+        // Set up the virtual link protocol
+        PhyProtocol phy = new PhyProtocol(SERVER_PORT);
+
+        // Set up command protocol
+        CPProtocol cp = null;
+        try {
+            cp = new CPProtocol(phy);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        // Start server processing
+        boolean eof = false;
+        while (!eof) {
+            try {
+                Msg msg = cp.receive();
+                String sentence = msg.getData().trim();
+                System.out.println("Received message: " + sentence);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/core/Configuration.java b/src/main/java/core/Configuration.java
new file mode 100644
index 0000000000000000000000000000000000000000..59ee4581d22bf200299ddd2e32063dbdbe6cbfb3
--- /dev/null
+++ b/src/main/java/core/Configuration.java
@@ -0,0 +1,21 @@
+package core;
+
+/*
+ * Abstract configuration class
+ * nextLowerProtocol attribute is currently unused (ignore)
+ */
+public class Configuration {
+	protected Protocol nextLowerProtocol;
+	
+	public Configuration(Protocol proto) {
+		this.nextLowerProtocol = proto;
+	}
+
+	public Protocol getNextLowerProtocol() {
+		return nextLowerProtocol;
+	}
+
+	private void setNextLowerProtocol(Protocol nextLowerProtocol) {
+		this.nextLowerProtocol = nextLowerProtocol;
+	}
+}
diff --git a/src/main/java/core/Msg.java b/src/main/java/core/Msg.java
new file mode 100644
index 0000000000000000000000000000000000000000..bf3be7213d97e3569b414bdf325274513100320d
--- /dev/null
+++ b/src/main/java/core/Msg.java
@@ -0,0 +1,42 @@
+package core;
+
+import exceptions.IWProtocolException;
+
+/* 
+ * Msg base class (abstract)
+ */
+public abstract class Msg {
+	protected String data;
+	protected byte[] dataBytes;
+	protected Configuration config;
+	
+	public byte[] getDataBytes() {
+		return this.dataBytes;
+	}
+	
+	public int getLength() {
+		return this.dataBytes.length;
+	}
+	
+	public String getData() {
+		return this.data;
+	}
+	
+	public void setData(String data) {
+		this.data = data;
+	}
+	
+	public Configuration getConfiguration() {
+		return this.config;
+	}
+	public void setConfiguration(Configuration c) {this.config = c;}
+	
+	protected abstract void create(String sentence);
+	protected abstract Msg parse(String sentence) throws IWProtocolException;
+	
+	
+	public void printDataBytes() {
+		String msgString = new String(this.dataBytes);
+		System.out.println(msgString);
+	}
+}
diff --git a/src/main/java/core/Protocol.java b/src/main/java/core/Protocol.java
new file mode 100644
index 0000000000000000000000000000000000000000..54be4ee55718cd3d5250a191fa54b45d8f715c13
--- /dev/null
+++ b/src/main/java/core/Protocol.java
@@ -0,0 +1,18 @@
+package core;
+
+import java.io.IOException;
+
+import exceptions.IWProtocolException;
+
+/*
+ * Protocol base class (abstract)
+ */
+public abstract class Protocol {
+	public enum proto_id {
+		PHY, APP, SLP, CP
+	}
+
+	public abstract void send(String s, Configuration config) throws IOException, IWProtocolException;
+	public abstract Msg receive() throws IOException, IWProtocolException;
+
+}
diff --git a/src/main/java/cp/CPCookieRequestMsg.java b/src/main/java/cp/CPCookieRequestMsg.java
new file mode 100644
index 0000000000000000000000000000000000000000..79a9e5236cd48b9e691f115755f4900ec98a1652
--- /dev/null
+++ b/src/main/java/cp/CPCookieRequestMsg.java
@@ -0,0 +1,28 @@
+package cp;
+
+import core.Msg;
+import exceptions.IllegalMsgException;
+
+class CPCookieRequestMsg extends CPMsg {
+    protected static final String CP_CREQ_HEADER = "creq";
+
+    /*
+     * Create cookie request message.
+     * The cp header is prepended in the super-class.
+     */
+    @Override
+    protected void create(String data) {
+        // prepend reg header
+        data = CP_CREQ_HEADER;
+        // super class prepends slp header
+        super.create(data);
+    }
+
+    @Override
+    protected Msg parse(String sentence) throws IllegalMsgException {
+        if (!sentence.startsWith(CP_CREQ_HEADER)) {
+            throw new IllegalMsgException();
+        }
+        return this;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/cp/CPCookieResponseMsg.java b/src/main/java/cp/CPCookieResponseMsg.java
new file mode 100644
index 0000000000000000000000000000000000000000..faeed5f46f10f3e4dc0e762f48dfabf16df8383c
--- /dev/null
+++ b/src/main/java/cp/CPCookieResponseMsg.java
@@ -0,0 +1,50 @@
+package cp;
+
+import core.Msg;
+import exceptions.IllegalMsgException;
+
+class CPCookieResponseMsg extends CPMsg {
+    protected static final String CP_CRES_HEADER = "cres";
+    private boolean success;
+
+    protected CPCookieResponseMsg() {
+
+    }
+    protected CPCookieResponseMsg(boolean s) {
+        this.success = s;
+    }
+
+    protected boolean getSuccess() {return this.success;}
+
+    /*
+     * Create cookie request message.
+     * The cp header is prepended in the super-class.
+     */
+    @Override
+    protected void create(String data) {
+        if (this.success) {
+            // prepend cres header
+            data = CP_CRES_HEADER + " ACK " + data;
+        } else {
+            data = CP_CRES_HEADER + " NAK " + data;
+        }
+        // super class prepends slp header
+        super.create(data);
+    }
+
+    protected Msg parse(String sentence) throws IllegalMsgException {
+        if (!sentence.startsWith(CP_CRES_HEADER)) {
+            throw new IllegalMsgException();
+        }
+        String[] parts = sentence.split("\\s+", 3);
+
+        if(parts[1].equals("ACK"))
+            this.success = true;
+        else
+            this.success = false;
+
+        this.data = parts[2];
+        return this;
+    }
+
+}
diff --git a/src/main/java/cp/CPMsg.java b/src/main/java/cp/CPMsg.java
new file mode 100644
index 0000000000000000000000000000000000000000..39f22905fc3ba2ef4c07a8290e4e5a0008012da2
--- /dev/null
+++ b/src/main/java/cp/CPMsg.java
@@ -0,0 +1,37 @@
+package cp;
+
+import core.Msg;
+import exceptions.IWProtocolException;
+import exceptions.IllegalMsgException;
+
+import java.util.zip.CRC32;
+import java.util.zip.Checksum;
+
+class CPMsg extends Msg {
+    protected static final String CP_HEADER = "cp";
+    @Override
+    protected void create(String sentence) {
+        this.data = sentence;
+        data = CP_HEADER + " " + sentence;
+        this.dataBytes = data.getBytes();
+    }
+
+    @Override
+    protected Msg parse(String sentence) throws IWProtocolException {
+        CPMsg parsedMsg;
+        if(!sentence.startsWith(CP_HEADER))
+            throw new IllegalMsgException();
+
+        String[] parts = sentence.split("\\s+", 2);
+        if(parts[1].startsWith(CPCookieRequestMsg.CP_CREQ_HEADER)) {
+            parsedMsg = new CPCookieRequestMsg();
+        } else if(parts[1].startsWith(CPCookieResponseMsg.CP_CRES_HEADER)) {
+            parsedMsg = new CPCookieResponseMsg();
+        } else
+            throw new IllegalMsgException();
+
+        parsedMsg = (CPMsg) parsedMsg.parse(parts[1]);
+        return parsedMsg;
+    }
+
+}
diff --git a/src/main/java/cp/CPProtocol.java b/src/main/java/cp/CPProtocol.java
new file mode 100644
index 0000000000000000000000000000000000000000..66e0f5e9622c0aa169e36a899b2adcc53d71ae02
--- /dev/null
+++ b/src/main/java/cp/CPProtocol.java
@@ -0,0 +1,76 @@
+package cp;
+
+import core.*;
+import exceptions.*;
+import phy.*;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Random;
+
+public class CPProtocol extends Protocol {
+    private static final int CP_TIMEOUT = 2000;
+    private static final int CP_HASHMAP_SIZE = 20;
+    private String cookie;
+    private int id;
+    private PhyConfiguration PhyConfig;
+    private PhyProtocol PhyProto;
+    boolean isClient;
+    HashMap<PhyConfiguration, Integer> cookieMap;
+    Random rnd;
+
+    // Constructor for clients
+    public CPProtocol(InetAddress rname, int rp, PhyProtocol phyP) throws UnknownHostException {
+        this.PhyConfig = new PhyConfiguration(rname, rp, proto_id.CP);
+        this.PhyProto = phyP;
+        this.isClient = true;
+    }
+    // Constructor for servers
+    public CPProtocol(PhyProtocol phyP) {
+        this.PhyProto = phyP;
+        this.isClient = false;
+        this.cookieMap = new HashMap<>();
+        this.rnd = new Random();
+    }
+    @Override
+    public void send(String s, Configuration config) throws IOException, IWProtocolException {
+        if (cookie == null) {
+            // Request a new cookie from server
+            // Either updates the cookie attribute or returns with an exception
+            requestCookie(0);
+        }
+    }
+
+    @Override
+    public Msg receive() throws IOException {
+        return null;
+    }
+
+
+     public void requestCookie(int count) throws IOException, IWProtocolException {
+        if(count >= 3)
+            throw new CookieRequestException();
+        CPCookieRequestMsg reqMsg = new CPCookieRequestMsg();
+        reqMsg.create(null);
+
+        this.PhyProto.send(new String (reqMsg.getDataBytes()), this.PhyConfig);
+        Msg resMsg = new CPMsg();
+
+        try {
+            Msg in = this.PhyProto.receive(CP_TIMEOUT);
+            if (((PhyConfiguration)in.getConfiguration()).getPid() != proto_id.CP)
+                throw new IllegalMsgException();
+            resMsg = ((CPMsg) resMsg).parse(in.getData());
+        } catch (SocketTimeoutException e) {
+            requestCookie(count+1);
+        }
+
+        if(resMsg instanceof CPCookieResponseMsg && !((CPCookieResponseMsg) resMsg).getSuccess()) {
+            throw new CookieRequestException();
+        }
+        this.cookie = resMsg.getData();
+    }
+}
diff --git a/src/main/java/exceptions/BadChecksumException.java b/src/main/java/exceptions/BadChecksumException.java
new file mode 100644
index 0000000000000000000000000000000000000000..397c97b87fd7939aff2f329889f42c6a86df3811
--- /dev/null
+++ b/src/main/java/exceptions/BadChecksumException.java
@@ -0,0 +1,10 @@
+package exceptions;
+
+public class BadChecksumException extends IWProtocolException {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -6481772554012497583L;
+
+}
diff --git a/src/main/java/exceptions/CookieRequestException.java b/src/main/java/exceptions/CookieRequestException.java
new file mode 100644
index 0000000000000000000000000000000000000000..a2c57679f515b8b1d4dcdbaf5e21c9ba6d7b9183
--- /dev/null
+++ b/src/main/java/exceptions/CookieRequestException.java
@@ -0,0 +1,4 @@
+package exceptions;
+
+public class CookieRequestException extends IWProtocolException {
+}
diff --git a/src/main/java/exceptions/IWProtocolException.java b/src/main/java/exceptions/IWProtocolException.java
new file mode 100644
index 0000000000000000000000000000000000000000..a1e895c799f7cc2ff5c20b45c154f0d3d14cb29e
--- /dev/null
+++ b/src/main/java/exceptions/IWProtocolException.java
@@ -0,0 +1,10 @@
+package exceptions;
+
+public abstract class IWProtocolException extends Exception {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -7002466204521265834L;
+
+}
diff --git a/src/main/java/exceptions/IllegalAddrException.java b/src/main/java/exceptions/IllegalAddrException.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa1a9b7d1a93be40322a3a00e410135bf1f0ac2d
--- /dev/null
+++ b/src/main/java/exceptions/IllegalAddrException.java
@@ -0,0 +1,12 @@
+package exceptions;
+
+import exceptions.IWProtocolException;
+
+public class IllegalAddrException extends IWProtocolException {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -2112808760774454967L;
+
+}
diff --git a/src/main/java/exceptions/IllegalCommandException.java b/src/main/java/exceptions/IllegalCommandException.java
new file mode 100644
index 0000000000000000000000000000000000000000..d1882d59fed81504104efe583c9dc130a353d4aa
--- /dev/null
+++ b/src/main/java/exceptions/IllegalCommandException.java
@@ -0,0 +1,4 @@
+package exceptions;
+
+public class IllegalCommandException extends IWProtocolException {
+}
diff --git a/src/main/java/exceptions/IllegalMsgException.java b/src/main/java/exceptions/IllegalMsgException.java
new file mode 100644
index 0000000000000000000000000000000000000000..9c5a1f5c913e874a5828a34416621f6fae0e3f85
--- /dev/null
+++ b/src/main/java/exceptions/IllegalMsgException.java
@@ -0,0 +1,10 @@
+package exceptions;
+
+public class IllegalMsgException extends IWProtocolException {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 2003522098885060854L;
+	
+}
diff --git a/src/main/java/exceptions/NoNextStateException.java b/src/main/java/exceptions/NoNextStateException.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca10c0ccf42f4a11673cd3c4d3568146653fde98
--- /dev/null
+++ b/src/main/java/exceptions/NoNextStateException.java
@@ -0,0 +1,10 @@
+package exceptions;
+
+public class NoNextStateException extends IWProtocolException {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 9032871496863489383L;
+
+}
diff --git a/src/main/java/exceptions/RegistrationFailedException.java b/src/main/java/exceptions/RegistrationFailedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..960f98500a138f9396acf4d6fba002b34671a9e5
--- /dev/null
+++ b/src/main/java/exceptions/RegistrationFailedException.java
@@ -0,0 +1,10 @@
+package exceptions;
+
+public class RegistrationFailedException extends IWProtocolException {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = -3028142631363949563L;
+
+}
diff --git a/src/main/java/phy/PhyConfiguration.java b/src/main/java/phy/PhyConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..e323121e59f39816f9893f823e7c876ecbb8fe0b
--- /dev/null
+++ b/src/main/java/phy/PhyConfiguration.java
@@ -0,0 +1,31 @@
+package phy;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import core.Configuration;
+import core.Protocol;
+
+public class PhyConfiguration extends Configuration{
+	protected int remotePort;
+	protected InetAddress remoteIPAddress;
+	protected Protocol.proto_id pid;
+	protected boolean isClient;
+	
+	public PhyConfiguration(InetAddress rip, int rp, Protocol.proto_id pid) throws UnknownHostException {
+		super(null);
+		this.remotePort = rp;
+		this.remoteIPAddress = rip;
+		this.pid = pid;
+		this.isClient = true;
+	}
+
+	public int getRemotePort() {
+		return this.remotePort;
+	}
+	
+	public InetAddress getRemoteIPAddress () {
+		return this.remoteIPAddress;
+	}
+	public Protocol.proto_id getPid() {return this.pid;}
+}
diff --git a/src/main/java/phy/PhyMsg.java b/src/main/java/phy/PhyMsg.java
new file mode 100644
index 0000000000000000000000000000000000000000..22dbeb60a6502d7615ce077a305d064afa3a5680
--- /dev/null
+++ b/src/main/java/phy/PhyMsg.java
@@ -0,0 +1,82 @@
+package phy;
+
+import core.Msg;
+import core.Protocol;
+import exceptions.IllegalMsgException;
+
+/*
+ * The only message class for the phy layer
+ * This layer prepends a "phy" header when sending a message
+ * This layer removes the "phy" header when receiving a message
+ */
+public class PhyMsg extends Msg {
+	protected static final String PHY_HEADER = "phy";
+	protected Protocol.proto_id pid;
+
+	protected PhyMsg() {}
+	protected PhyMsg(PhyConfiguration config) {
+		super();
+		this.config = config;
+	}
+
+	protected Protocol.proto_id getPid() {return this.pid;}
+
+	/*
+	 * Prepend header for sending
+	 */
+	@Override
+	protected void create(String data) {
+		this.data = data;
+		int id;
+		PhyConfiguration conf = (PhyConfiguration) this.config;
+		id = switch (conf.getPid()) {
+			case PHY -> 1;
+			case APP -> 3;
+			case SLP -> 5;
+			case CP -> 7;
+		};
+		data = PHY_HEADER + " " + id + " " +data;
+		this.dataBytes = data.getBytes();
+	}
+	
+	/*
+	 * Does the message start with the correct header
+	 *  -> if not illegal message
+	 * Remove header and populate data attribute
+	 */
+	@Override
+	protected Msg parse(String sentence) throws IllegalMsgException {
+		this.dataBytes = sentence.getBytes();
+		if (!sentence.startsWith(PHY_HEADER)) {
+			System.out.println("Illeagal data header: " + sentence);
+			throw new IllegalMsgException();
+		}
+		String[] parts = sentence.split("\\s+", 3);
+		PhyMsg pdu;
+		// Parse the protocol id
+		int id;
+		try {
+			id = Integer.parseInt(parts[1]);
+		} catch (NumberFormatException e) {
+			throw new IllegalMsgException();
+		}
+		//Check protocol id
+		switch (id) {
+			case 1 -> pid = Protocol.proto_id.PHY;
+			case 3 -> pid = Protocol.proto_id.APP;
+			case 5 -> pid = Protocol.proto_id.SLP;
+			case 7 -> pid = Protocol.proto_id.CP;
+			default -> throw new IllegalMsgException();
+		}
+		// If the second token is "1", call the PhyPingMsg parser
+		if(pid == Protocol.proto_id.PHY && parts[2].startsWith(PhyPingMsg.PHY_PING_HEADER)) {
+			pdu = new PhyPingMsg((PhyConfiguration) this.config);
+			pdu.parse(parts[2]);
+		} else {
+			this.data = parts[2];
+			pdu = this;
+		}
+		return pdu;
+	}
+	
+}
diff --git a/src/main/java/phy/PhyPingMsg.java b/src/main/java/phy/PhyPingMsg.java
new file mode 100644
index 0000000000000000000000000000000000000000..51790fddd4287983686d792b1ad0a483271b9b11
--- /dev/null
+++ b/src/main/java/phy/PhyPingMsg.java
@@ -0,0 +1,51 @@
+package phy;
+
+import core.Msg;
+import exceptions.IllegalMsgException;
+
+public class PhyPingMsg extends PhyMsg {
+	protected static final String PHY_PING_HEADER = "ping ";
+	private int count;
+
+	protected PhyPingMsg(PhyConfiguration config) {
+		super(config);
+	}
+	
+	public int getCount() {
+		return count;
+	}
+
+	/*
+	 * Create ping msg
+	 */
+	@Override
+	protected void create(String data) {
+		this.data = data;
+		String sentence = PHY_PING_HEADER + data;
+		super.create(sentence);
+	}
+	
+	/*
+	 * Does the message start with the correct header
+	 *  -> if not illegal message
+	 * Remove header and populate data attribute
+	 *  -> try converting data to type Integer
+	 */
+	@Override
+	protected Msg parse(String sentence) throws IllegalMsgException {
+		this.dataBytes = sentence.getBytes();
+		if (!sentence.startsWith(PHY_PING_HEADER)) {
+			System.out.println("Illeagal ping header: " + sentence);
+			throw new IllegalMsgException();
+		}
+		this.data = sentence.substring(PHY_PING_HEADER.length());
+		try {
+			this.count = Integer.parseInt(this.data.trim());
+		} catch (NumberFormatException e) {
+			throw new IllegalMsgException();
+		}
+		
+		return this;
+	}
+
+}
diff --git a/src/main/java/phy/PhyProtocol.java b/src/main/java/phy/PhyProtocol.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a2eb0344a13d63c62257d18f15846fadb1ed70c
--- /dev/null
+++ b/src/main/java/phy/PhyProtocol.java
@@ -0,0 +1,104 @@
+package phy;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.util.concurrent.TimeoutException;
+
+import core.*;
+import exceptions.*;
+
+public class PhyProtocol extends Protocol {
+	protected DatagramSocket socket;
+	
+	/*
+	 * Create a new PhyProtocol instance connected to UDP port provided
+	 */
+	public PhyProtocol(int port) {
+		try {
+			this.socket = new DatagramSocket(port);
+		} catch (SocketException e) {
+			e.printStackTrace();
+		}
+	}
+
+	/*
+	 * Create msg object and send
+	 */
+	@Override
+	public void send(String s, Configuration config) throws IOException, IWProtocolException {
+		// Create empty PhyMsg object
+		PhyMsg m = new PhyMsg((PhyConfiguration) config);
+		//Populate PhyMsg object with data
+		m.create(s);
+		// Call actual send method
+		this.send(m);
+	}
+	
+	public void send(PhyMsg m) throws IOException, IWProtocolException {	
+		// Create UDP packet
+		DatagramPacket sendPacket = new DatagramPacket(m.getDataBytes(), m.getLength(),
+				((PhyConfiguration) m.getConfiguration()).remoteIPAddress,
+				((PhyConfiguration) m.getConfiguration()).remotePort);
+		// send UDP packet
+		socket.send(sendPacket);
+	}
+	
+	/*
+	 * receive incoming message from socket and parse -> call blocks on socket until message is received
+	 * return Msg object to caller
+	 */
+	@Override
+	public Msg receive() throws IOException {
+		// read from UDP socket
+		// data and meta-data contained in receivedPacket object
+		byte[] receiveData = new byte[1024];
+		DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
+		socket.receive(receivePacket);
+
+		// create msg object for parsing
+		PhyMsg in = new PhyMsg();
+		// get data from packet data
+		String sentence = new String(receivePacket.getData()).trim();
+		try {
+			// parse data to check if message is compliant with protocol specification
+			in = (PhyMsg) in.parse(sentence);
+		} catch (IllegalMsgException e) {
+			e.printStackTrace();
+		}
+		// create a config object from packet meta-data
+		PhyConfiguration config = new PhyConfiguration(receivePacket.getAddress(), receivePacket.getPort(), in.getPid());
+		in.setConfiguration(config);
+
+		// if message was parsed correctly object is returned to caller  
+		return in;
+	}
+	
+	/*
+	 * wrapper method to basic receive method -> call blocks on socket until message is received or 
+	 * timeout expires and exception is raised
+	 */
+	public Msg receive(int timeout) throws IOException {
+		socket.setSoTimeout(timeout);
+		Msg in;
+		in = receive();
+
+		socket.setSoTimeout(0);
+		return in;
+	}
+	
+	// Send three ping messages to another system
+	public void ping(Configuration config) throws IOException, IWProtocolException {
+		for(int i=0; i<3;i++) {
+			// Create empty PhyPingMsg object
+			PhyPingMsg m = new PhyPingMsg((PhyConfiguration) config);
+			//Populate PhyPingMsg object with data
+			m.create(Integer.toString(i));
+			// Call actual send method
+			this.send(m);
+		}
+	}
+	
+}
diff --git a/src/test/java/phy/CPClientCookieRequestTest.java b/src/test/java/phy/CPClientCookieRequestTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..dbe591dd32299aa5cdb2384156e5ce0d72deb60e
--- /dev/null
+++ b/src/test/java/phy/CPClientCookieRequestTest.java
@@ -0,0 +1,149 @@
+package phy;
+
+import core.Protocol;
+import cp.CPProtocol;
+import exceptions.CookieRequestException;
+import exceptions.IWProtocolException;
+import exceptions.IllegalMsgException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class CPClientCookieRequestTest {
+    String serverName = "localhost";
+    int serverPort = 3027;
+
+    @Mock
+    PhyProtocol phyProtocolMock;
+    PhyMsg testMsg;
+    CPProtocol cProtocol;
+
+    @BeforeEach
+    void setup() throws UnknownHostException {
+        // Create additionally needed objects for every test
+        PhyConfiguration phyConfig;
+        try {
+            phyConfig = new PhyConfiguration(InetAddress.getByName(serverName), serverPort, Protocol.proto_id.CP);
+            testMsg = new PhyMsg(phyConfig);
+        } catch (UnknownHostException e) {
+            fail();
+        }
+        // Set up the object-under-test
+        cProtocol = new CPProtocol(InetAddress.getByName(serverName), serverPort, phyProtocolMock);
+    }
+
+    @Test
+    void testCookieRequestSuccessful() throws IWProtocolException, IOException {
+        // Fill the message object that is going to be returned to the object-under-test
+        // with the message needed for this test case
+        testMsg = (PhyMsg)testMsg.parse("phy 7 cp cres ACK 12345");
+
+        // Implement behavior of the mocked object
+        when(phyProtocolMock.receive(anyInt())).thenReturn(testMsg);
+
+        // Run the test
+        assertDoesNotThrow(()->cProtocol.requestCookie(0));
+
+        // verify a specified behavior
+        verify(phyProtocolMock, times(1)).receive(2000);
+        verify(phyProtocolMock).send(eq("cp creq"), any(PhyConfiguration.class));
+    }
+
+    @Test
+    void testNoCookie() throws IOException, IWProtocolException {
+
+        // Fill the message object that is going to be returned to the object-under-test
+        // with the message needed for this test case
+        testMsg = (PhyMsg) testMsg.parse("phy 7 cp cres NAK no resources");
+
+        // Implement behavior of the mocked object
+        when(phyProtocolMock.receive(anyInt())).thenReturn(testMsg);
+
+        // Run the test
+        assertThrows(CookieRequestException.class,
+                ()->cProtocol.requestCookie(0));
+        verify(phyProtocolMock, times(1)).receive(2000);
+
+    }
+
+    @Test
+    void testIllegalPhyMsg() throws IOException, IWProtocolException {
+
+        // Fill the message object that is going to be returned to the object-under-test
+        // with the message needed for this test case (also create corresponding configuration object)
+        PhyConfiguration phyConfig = new PhyConfiguration(InetAddress.getByName(serverName), serverPort, Protocol.proto_id.SLP);
+        testMsg = new PhyMsg(phyConfig);
+        testMsg = (PhyMsg) testMsg.parse("phy 5 cp cres ACK 12345");
+
+        // Implement behavior of the mocked object
+        when(phyProtocolMock.receive(anyInt())).thenReturn(testMsg);
+
+        // Run the test
+        assertThrows(IllegalMsgException.class,
+                ()->cProtocol.requestCookie(0));
+        verify(phyProtocolMock, times(1)).receive(2000);
+    }
+
+    @Test
+    void testMaleformedCPMsg() throws IOException, IWProtocolException {
+
+        // Fill the message object that is going to be returned to the object-under-test
+        // with the message needed for this test case (also create corresponding configuration object)
+        PhyConfiguration phyConfig = new PhyConfiguration(InetAddress.getByName(serverName), serverPort, Protocol.proto_id.SLP);
+        testMsg = new PhyMsg(phyConfig);
+        testMsg = (PhyMsg) testMsg.parse("phy 7 cp cresACK 12345");
+
+        // Implement behavior of the mocked object
+        when(phyProtocolMock.receive(anyInt())).thenReturn(testMsg);
+
+        // Run the test
+        assertThrows(IllegalMsgException.class,
+                ()->cProtocol.requestCookie(0));
+        verify(phyProtocolMock, times(1)).receive(2000);
+    }
+
+    @Test
+    void testIncompleteCPMsg() throws IOException, IWProtocolException {
+
+        // Fill the message object that is going to be returned to the object-under-test
+        // with the message needed for this test case (also create corresponding configuration object)
+        PhyConfiguration phyConfig = new PhyConfiguration(InetAddress.getByName(serverName), serverPort, Protocol.proto_id.SLP);
+        testMsg = new PhyMsg(phyConfig);
+        testMsg = (PhyMsg) testMsg.parse("phy 5 cp cres ACK");
+
+        // Implement behavior of the mocked object
+        when(phyProtocolMock.receive(anyInt())).thenReturn(testMsg);
+
+        // Run the test
+        assertThrows(IllegalMsgException.class,
+                ()->cProtocol.requestCookie(0));
+        verify(phyProtocolMock, times(1)).receive(2000);
+    }
+
+    @Test
+    void testMessageLoss() throws IOException, IWProtocolException {
+
+        // Implement behavior of the mocked object
+        when(phyProtocolMock.receive(anyInt())).thenThrow(new SocketTimeoutException());
+
+        // Run the test
+        assertThrows(CookieRequestException.class,
+                ()->cProtocol.requestCookie(0));
+        verify(phyProtocolMock, times(3)).receive(2000);
+    }
+
+}