Repository: antrea-io/antrea
Pull request #2659 (2021-08-27 00:31:13)

Update Audit Logging Documentation and Format
This PR follows [the previous PR](https://github.com/antrea-io/antrea/pull/1477) to update the documentation for audit logging. Recently deduplication is added for drop and reject packets.

Also, there was a discussion in the PR about removing `SRC: DEST:` in the log to keep consistency.


Patch:
diff --git a/build/images/base/Dockerfile b/build/images/base/Dockerfile
index e2b25ecfc..56d94a3d9 100644
--- a/build/images/base/Dockerfile
+++ b/build/images/base/Dockerfile
@@ -2,6 +2,7 @@ ARG OVS_VERSION
 FROM ubuntu:20.04 as cni-binaries
 
 ARG CNI_BINARIES_VERSION
+ARG WHEREABOUTS_VERSION=v0.4.2
 
 RUN apt-get update && \
     apt-get install -y --no-install-recommends wget ca-certificates
@@ -20,7 +21,7 @@ RUN set -eux; \
     esac; \
     mkdir -p /opt/cni/bin; \
     wget -q -O - https://github.com/containernetworking/plugins/releases/download/$CNI_BINARIES_VERSION/cni-plugins-linux-${pluginsArch}-$CNI_BINARIES_VERSION.tgz | tar xz -C /opt/cni/bin $CNI_PLUGINS; \
-    wget -q -O - https://downloads.antrea.io/whereabouts/v0.4/whereabouts-linux-${pluginsArch}.tgz | tar xz -C /opt/cni/bin/ whereabouts-linux-${pluginsArch}/whereabouts --strip-components=1 --no-same-owner
+    wget -q -O - https://downloads.antrea.io/whereabouts/$WHEREABOUTS_VERSION/whereabouts-linux-${pluginsArch}.tgz | tar xz -C /opt/cni/bin/ whereabouts-linux-${pluginsArch}/whereabouts --strip-components=1 --no-same-owner
 
 FROM antrea/openvswitch:${OVS_VERSION}
 
diff --git a/build/images/scripts/install_cni b/build/images/scripts/install_cni
index 336e53cb0..a464662ef 100755
--- a/build/images/scripts/install_cni
+++ b/build/images/scripts/install_cni
@@ -33,5 +33,9 @@ install -m 755 /opt/cni/bin/whereabouts /host/opt/cni/bin/whereabouts
 # Load the OVS kernel module
 modprobe openvswitch || (echo "Failed to load the OVS kernel module from the container, try running 'modprobe openvswitch' on your Nodes"; exit 1)
 
+# Load the WireGuard kernel module. This is only required when WireGuard encryption is enabled.
+# We could parse the antrea config file in the init-container to dynamically load this kernel module in the future.
+modprobe wireguard || (echo "Failed to load the WireGuard kernel module, WireGuard encryption will not be available")
+
 # Change the default permissions of the run directory.
 chmod 0750 /var/run/antrea
diff --git a/build/images/wireguard-go/Dockerfile b/build/images/wireguard-go/Dockerfile
new file mode 100644
index 000000000..71e8aba88
--- /dev/null
+++ b/build/images/wireguard-go/Dockerfile
@@ -0,0 +1,19 @@
+ARG GO_VERSION
+ARG WIREGUARD_GO_VERSION
+
+FROM golang:${GO_VERSION} as builder
+
+RUN git clone https://git.zx2c4.com/wireguard-go && \
+    cd wireguard-go && \
+    git checkout ${WIREGUARD_GO_VERSION} && \
+    make && \ 
+    make install
+
+RUN git clone https://git.zx2c4.com/wireguard-tools && \
+    cd wireguard-tools && \
+    cd src && \
+    make && \
+    make install
+
+FROM ubuntu:20.04
+COPY --from=builder /usr/bin/wireguard-go /usr/bin/wg /usr/bin/
diff --git a/build/images/wireguard-go/README.md b/build/images/wireguard-go/README.md
new file mode 100644
index 000000000..66abe5964
--- /dev/null
+++ b/build/images/wireguard-go/README.md
@@ -0,0 +1,20 @@
+# images/wireguard-go
+
+This Docker image is a very lightweight image based on Ubuntu 20.04 which
+includes WireGuard golang implementation and wireguard-tools. It can be used
+for Kind clusters for tests when injected as a sidecar to antrea-agent.
+The version is available at <https://github.com/WireGuard/wireguard-go/releases>.
+
+If you need to build a new version of the image and push it to Dockerhub, you
+can run the following:
+
+```bash
+cd build/images/wireguard-go
+GO_VERSION=$(head -n 1 ../deps/go-version)
+WIREGUARD_GO_VERSION=0.0.20210424
+docker build -t antrea/wireguard-go:$WIREGUARD_GO_VERSION --build-arg GO_VERSION=$GO_VERSION --build-arg WIREGUARD_GO_VERSION=$WIREGUARD_GO_VERSION .
+docker push antrea/wireguard-go:$WIREGUARD_GO_VERSION
+```
+
+The `docker push` command will fail if you do not have permission to push to the
+`antrea` Dockerhub repository.
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index 85d73d60e..6a823bbe3 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -3820,20 +3820,33 @@ data:
     # performs SNAT and this option will be ignored; for other modes it must be set to false.
     #noSNAT: false
 
-    # Tunnel protocols used for encapsulating traffic across Nodes. Supported values:
+    # Tunnel protocols used for encapsulating traffic across Nodes. If WireGuard is enabled in trafficEncryptionMode,
+    # this option will not take effect. Supported values:
     # - geneve (default)
     # - vxlan
     # - gre
     # - stt
     #tunnelType: geneve
 
+    # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
+    # It has the following options:
+    # - none (default):  Inter-node Pod traffic will not be encrypted.
+    # - ipsec:           Enable IPSec (ESP) encryption for Pod traffic across Nodes. Antrea uses
+    #                    Preshared Key (PSK) for IKE authentication. When IPSec tunnel is enabled,
+    #                    the PSK value must be passed to Antrea Agent through an environment
+    #                    variable: ANTREA_IPSEC_PSK.
+    # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
+    #trafficEncryptionMode: none
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
     #defaultMTU: 0
 
-    # Whether or not to enable IPsec encryption of tunnel traffic.
-    #enableIPSecTunnel: false
+    # wireGuard specifies WireGuard related configurations.
+    wireGuard:
+    #  The port for WireGuard to receive traffic.
+    #  port: 51820
 
     # ClusterIP CIDR range for IPv6 Services. It's required when using kube-proxy to provide IPv6 Service in a Dual-Stack
     # cluster or an IPv6 only cluster. The value should be the same as the configuration for kube-apiserver specified by
@@ -3983,7 +3996,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-66dt98cgtb
+  name: antrea-config-99c875tk88
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4054,7 +4067,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-66dt98cgtb
+          value: antrea-config-99c875tk88
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4105,7 +4118,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-66dt98cgtb
+          name: antrea-config-99c875tk88
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4386,7 +4399,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-66dt98cgtb
+          name: antrea-config-99c875tk88
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index 04df581d1..6d246c3fa 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -3820,20 +3820,33 @@ data:
     # performs SNAT and this option will be ignored; for other modes it must be set to false.
     #noSNAT: false
 
-    # Tunnel protocols used for encapsulating traffic across Nodes. Supported values:
+    # Tunnel protocols used for encapsulating traffic across Nodes. If WireGuard is enabled in trafficEncryptionMode,
+    # this option will not take effect. Supported values:
     # - geneve (default)
     # - vxlan
     # - gre
     # - stt
     #tunnelType: geneve
 
+    # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
+    # It has the following options:
+    # - none (default):  Inter-node Pod traffic will not be encrypted.
+    # - ipsec:           Enable IPSec (ESP) encryption for Pod traffic across Nodes. Antrea uses
+    #                    Preshared Key (PSK) for IKE authentication. When IPSec tunnel is enabled,
+    #                    the PSK value must be passed to Antrea Agent through an environment
+    #                    variable: ANTREA_IPSEC_PSK.
+    # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
+    #trafficEncryptionMode: none
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
     #defaultMTU: 0
 
-    # Whether or not to enable IPsec encryption of tunnel traffic.
-    #enableIPSecTunnel: false
+    # wireGuard specifies WireGuard related configurations.
+    wireGuard:
+    #  The port for WireGuard to receive traffic.
+    #  port: 51820
 
     # ClusterIP CIDR range for IPv6 Services. It's required when using kube-proxy to provide IPv6 Service in a Dual-Stack
     # cluster or an IPv6 only cluster. The value should be the same as the configuration for kube-apiserver specified by
@@ -3983,7 +3996,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-66dt98cgtb
+  name: antrea-config-99c875tk88
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4054,7 +4067,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-66dt98cgtb
+          value: antrea-config-99c875tk88
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4105,7 +4118,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-66dt98cgtb
+          name: antrea-config-99c875tk88
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4388,7 +4401,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-66dt98cgtb
+          name: antrea-config-99c875tk88
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index 5e4ba0b8c..c0f0f8f0f 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -3820,20 +3820,33 @@ data:
     # performs SNAT and this option will be ignored; for other modes it must be set to false.
     #noSNAT: false
 
-    # Tunnel protocols used for encapsulating traffic across Nodes. Supported values:
+    # Tunnel protocols used for encapsulating traffic across Nodes. If WireGuard is enabled in trafficEncryptionMode,
+    # this option will not take effect. Supported values:
     # - geneve (default)
     # - vxlan
     # - gre
     # - stt
     #tunnelType: geneve
 
+    # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
+    # It has the following options:
+    # - none (default):  Inter-node Pod traffic will not be encrypted.
+    # - ipsec:           Enable IPSec (ESP) encryption for Pod traffic across Nodes. Antrea uses
+    #                    Preshared Key (PSK) for IKE authentication. When IPSec tunnel is enabled,
+    #                    the PSK value must be passed to Antrea Agent through an environment
+    #                    variable: ANTREA_IPSEC_PSK.
+    # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
+    #trafficEncryptionMode: none
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
     #defaultMTU: 0
 
-    # Whether or not to enable IPsec encryption of tunnel traffic.
-    #enableIPSecTunnel: false
+    # wireGuard specifies WireGuard related configurations.
+    wireGuard:
+    #  The port for WireGuard to receive traffic.
+    #  port: 51820
 
     # ClusterIP CIDR range for IPv6 Services. It's required when using kube-proxy to provide IPv6 Service in a Dual-Stack
     # cluster or an IPv6 only cluster. The value should be the same as the configuration for kube-apiserver specified by
@@ -3983,7 +3996,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-d2f597tg62
+  name: antrea-config-dbmkcb65c8
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4054,7 +4067,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-d2f597tg62
+          value: antrea-config-dbmkcb65c8
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4105,7 +4118,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-d2f597tg62
+          name: antrea-config-dbmkcb65c8
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4389,7 +4402,7 @@ spec:
           path: /home/kubernetes/bin
         name: host-cni-bin
       - configMap:
-          name: antrea-config-d2f597tg62
+          name: antrea-config-dbmkcb65c8
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index d338d8e63..c299f230e 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -3820,20 +3820,33 @@ data:
     # performs SNAT and this option will be ignored; for other modes it must be set to false.
     #noSNAT: false
 
-    # Tunnel protocols used for encapsulating traffic across Nodes. Supported values:
+    # Tunnel protocols used for encapsulating traffic across Nodes. If WireGuard is enabled in trafficEncryptionMode,
+    # this option will not take effect. Supported values:
     # - geneve (default)
     # - vxlan
     # - gre
     # - stt
     tunnelType: gre
 
+    # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
+    # It has the following options:
+    # - none (default):  Inter-node Pod traffic will not be encrypted.
+    # - ipsec:           Enable IPSec (ESP) encryption for Pod traffic across Nodes. Antrea uses
+    #                    Preshared Key (PSK) for IKE authentication. When IPSec tunnel is enabled,
+    #                    the PSK value must be passed to Antrea Agent through an environment
+    #                    variable: ANTREA_IPSEC_PSK.
+    # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
+    trafficEncryptionMode: ipsec
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
     #defaultMTU: 0
 
-    # Whether or not to enable IPsec encryption of tunnel traffic.
-    enableIPSecTunnel: true
+    # wireGuard specifies WireGuard related configurations.
+    wireGuard:
+    #  The port for WireGuard to receive traffic.
+    #  port: 51820
 
     # ClusterIP CIDR range for Services. It's required when AntreaProxy is not enabled, and should be
     # set to the same value as the one specified by --service-cluster-ip-range for kube-apiserver. When
@@ -3988,7 +4001,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-bgd79km9c8
+  name: antrea-config-tthkbhb7k5
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4068,7 +4081,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-bgd79km9c8
+          value: antrea-config-tthkbhb7k5
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4119,7 +4132,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-bgd79km9c8
+          name: antrea-config-tthkbhb7k5
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4435,7 +4448,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-bgd79km9c8
+          name: antrea-config-tthkbhb7k5
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index ba4fab70c..d95ecbbc5 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -3820,20 +3820,33 @@ data:
     # performs SNAT and this option will be ignored; for other modes it must be set to false.
     #noSNAT: false
 
-    # Tunnel protocols used for encapsulating traffic across Nodes. Supported values:
+    # Tunnel protocols used for encapsulating traffic across Nodes. If WireGuard is enabled in trafficEncryptionMode,
+    # this option will not take effect. Supported values:
     # - geneve (default)
     # - vxlan
     # - gre
     # - stt
     #tunnelType: geneve
 
+    # Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
+    # It has the following options:
+    # - none (default):  Inter-node Pod traffic will not be encrypted.
+    # - ipsec:           Enable IPSec (ESP) encryption for Pod traffic across Nodes. Antrea uses
+    #                    Preshared Key (PSK) for IKE authentication. When IPSec tunnel is enabled,
+    #                    the PSK value must be passed to Antrea Agent through an environment
+    #                    variable: ANTREA_IPSEC_PSK.
+    # - wireGuard:       Enable WireGuard for tunnel traffic encryption.
+    #trafficEncryptionMode: none
+
     # Default MTU to use for the host gateway interface and the network interface of each Pod.
     # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
     # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
     #defaultMTU: 0
 
-    # Whether or not to enable IPsec encryption of tunnel traffic.
-    #enableIPSecTunnel: false
+    # wireGuard specifies WireGuard related configurations.
+    wireGuard:
+    #  The port for WireGuard to receive traffic.
+    #  port: 51820
 
     # ClusterIP CIDR range for Services. It's required when AntreaProxy is not enabled, and should be
     # set to the same value as the one specified by --service-cluster-ip-range for kube-apiserver. When
@@ -3988,7 +4001,7 @@ metadata:
   annotations: {}
   labels:
     app: antrea
-  name: antrea-config-dd8ffc8tk9
+  name: antrea-config-mc8h75hbgg
   namespace: kube-system
 ---
 apiVersion: v1
@@ -4059,7 +4072,7 @@ spec:
             fieldRef:
               fieldPath: spec.serviceAccountName
         - name: ANTREA_CONFIG_MAP_NAME
-          value: antrea-config-dd8ffc8tk9
+          value: antrea-config-mc8h75hbgg
         image: projects.registry.vmware.com/antrea/antrea-ubuntu:latest
         imagePullPolicy: IfNotPresent
         livenessProbe:
@@ -4110,7 +4123,7 @@ spec:
         key: node-role.kubernetes.io/master
       volumes:
       - configMap:
-          name: antrea-config-dd8ffc8tk9
+          name: antrea-config-mc8h75hbgg
         name: antrea-config
       - name: antrea-controller-tls
         secret:
@@ -4391,7 +4404,7 @@ spec:
         operator: Exists
       volumes:
       - configMap:
-          name: antrea-config-dd8ffc8tk9
+          name: antrea-config-mc8h75hbgg
         name: antrea-config
       - hostPath:
           path: /etc/cni/net.d
diff --git a/build/yamls/base/conf/antrea-agent.conf b/build/yamls/base/conf/antrea-agent.conf
index 36193814a..43488921c 100644
--- a/build/yamls/base/conf/antrea-agent.conf
+++ b/build/yamls/base/conf/antrea-agent.conf
@@ -65,20 +65,33 @@ featureGates:
 # performs SNAT and this option will be ignored; for other modes it must be set to false.
 #noSNAT: false
 
-# Tunnel protocols used for encapsulating traffic across Nodes. Supported values:
+# Tunnel protocols used for encapsulating traffic across Nodes. If WireGuard is enabled in trafficEncryptionMode,
+# this option will not take effect. Supported values:
 # - geneve (default)
 # - vxlan
 # - gre
 # - stt
 #tunnelType: geneve
 
+# Determines how tunnel traffic is encrypted. Currently encryption only works with encap mode.
+# It has the following options:
+# - none (default):  Inter-node Pod traffic will not be encrypted.
+# - ipsec:           Enable IPSec (ESP) encryption for Pod traffic across Nodes. Antrea uses
+#                    Preshared Key (PSK) for IKE authentication. When IPSec tunnel is enabled,
+#                    the PSK value must be passed to Antrea Agent through an environment
+#                    variable: ANTREA_IPSEC_PSK.
+# - wireGuard:       Enable WireGuard for tunnel traffic encryption.
+#trafficEncryptionMode: none
+
 # Default MTU to use for the host gateway interface and the network interface of each Pod.
 # If omitted, antrea-agent will discover the MTU of the Node's primary interface and
 # also adjust MTU to accommodate for tunnel encapsulation overhead (if applicable).
 #defaultMTU: 0
 
-# Whether or not to enable IPsec encryption of tunnel traffic.
-#enableIPSecTunnel: false
+# wireGuard specifies WireGuard related configurations.
+wireGuard:
+#  The port for WireGuard to receive traffic.
+#  port: 51820
 
 # ClusterIP CIDR range for Services. It's required when AntreaProxy is not enabled, and should be
 # set to the same value as the one specified by --service-cluster-ip-range for kube-apiserver. When
diff --git a/build/yamls/patches/kind/wireguardGo.yml b/build/yamls/patches/kind/wireguardGo.yml
new file mode 100644
index 000000000..6d42f38c0
--- /dev/null
+++ b/build/yamls/patches/kind/wireguardGo.yml
@@ -0,0 +1,39 @@
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: antrea-agent
+spec:
+  template:
+    spec:
+      containers:
+      - name: wireguard
+        command: 
+        - wireguard-go
+        args: 
+        - -f
+        - antrea-wg0
+        image: projects.registry.vmware.com/antrea/wireguard-go:0.0.20210424
+        imagePullPolicy: IfNotPresent
+        resources:
+          requests:
+            cpu: 100m
+        securityContext:
+          capabilities:
+            add:
+            - NET_ADMIN
+        volumeMounts:
+        - mountPath: /var/run/wireguard
+          name: host-var-run-antrea
+          subPath: wireguard
+        - name: dev-tun
+          mountPath: /dev/net/tun
+      - name: antrea-agent
+        volumeMounts:
+        - mountPath: /var/run/wireguard
+          name: host-var-run-antrea
+          subPath: wireguard
+      volumes:
+      - name: dev-tun
+        hostPath:
+          path: /dev/net/tun
+          type: CharDevice
diff --git a/ci/jenkins/mellanox/common/common_functions.sh b/ci/jenkins/mellanox/common/common_functions.sh
index 61370201c..bb0381c6f 100755
--- a/ci/jenkins/mellanox/common/common_functions.sh
+++ b/ci/jenkins/mellanox/common/common_functions.sh
@@ -414,6 +414,8 @@ function build_github_project {
 
     git clone "${!repo_variable}" "$WORKSPACE"/"$project_name"
 
+    local sed_match_reg='s/(@| |^|=|	)docker($| )/\1sudo docker\2/'
+
     pushd $WORKSPACE/"$project_name"
     # Check if a pull request or a branch of the project is specified, if so build the image, otherwise pull
     # the image from the custom registry.
@@ -425,6 +427,9 @@ function build_github_project {
             echo "ERROR: Failed to checkout the $project_name pull request number ${!pr_variable}!"
             return "$status"
         fi
+
+        sed -ri "${sed_match_reg}" Makefile
+
         eval "$image_build_command"
         let status=$status+$?
     elif test ${!branch_variable}; then
@@ -434,6 +439,9 @@ function build_github_project {
             echo "ERROR: Failed to checkout the $project_name branch ${!branch_variable}!"
             return "$status"
         fi
+
+        sed -ri "${sed_match_reg}" Makefile
+
         eval "$image_build_command"
         let status=$status+$?
     else
diff --git a/ci/jenkins/mellanox/scripts/start_ci.sh b/ci/jenkins/mellanox/scripts/start_ci.sh
index f16746fc4..4ba57cd37 100755
--- a/ci/jenkins/mellanox/scripts/start_ci.sh
+++ b/ci/jenkins/mellanox/scripts/start_ci.sh
@@ -77,12 +77,16 @@ EOF
         fi
         cp -rf "$antrea_scm_dir" $WORKSPACE/antrea-cni
         pushd $WORKSPACE/antrea-cni
+
+        local sed_match_reg='s/(@| |^|=|	)docker($| )/\1sudo docker\2/'
+
+        sed -ri "${sed_match_reg}" Makefile
         
-        sudo docker build -t "$ANTREA_CNI_HARBOR_IMAGE" -f build/images/Dockerfile.build.ubuntu --build-arg OVS_VERSION=$(head -n 1 build/images/deps/ovs-version) .
+        make build-ubuntu
         let status=status+$?
         popd
     else
-        build_github_project "antrea-cni" "sudo docker build -t "$ANTREA_CNI_HARBOR_IMAGE" -f build/images/Dockerfile.build.ubuntu --build-arg OVS_VERSION=$(head -n 1 build/images/deps/ovs-version) ."
+        build_github_project "antrea-cni" "make build-ubuntu"
         let status=status+$?
     fi
 
@@ -91,6 +95,8 @@ EOF
         return $status
     fi
 
+    sudo docker tag antrea/antrea-ubuntu "$ANTREA_CNI_HARBOR_IMAGE"
+
     IMG_NAME="$ANTREA_CNI_HARBOR_IMAGE" bash $WORKSPACE/antrea-cni/hack/generate-manifest.sh --hw-offload > $ARTIFACTS/antrea.yml
     let status=status+$?
     if [ "$status" != 0 ]; then
diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go
index 4fb427552..f63c15542 100644
--- a/cmd/antrea-agent/agent.go
+++ b/cmd/antrea-agent/agent.go
@@ -114,13 +114,21 @@ func run(o *Options) error {
 	}
 
 	_, encapMode := config.GetTrafficEncapModeFromStr(o.config.TrafficEncapMode)
+	_, encryptionMode := config.GetTrafficEncryptionModeFromStr(o.config.TrafficEncryptionMode)
+	if o.config.EnableIPSecTunnel {
+		klog.Warning("enableIPSecTunnel is deprecated, use trafficEncryptionMode instead.")
+		encryptionMode = config.TrafficEncryptionModeIPSec
+	}
 	networkConfig := &config.NetworkConfig{
-		TunnelType:        ovsconfig.TunnelType(o.config.TunnelType),
-		TrafficEncapMode:  encapMode,
-		EnableIPSecTunnel: o.config.EnableIPSecTunnel,
-		TransportIface:    o.config.TransportInterface,
+		TunnelType:            ovsconfig.TunnelType(o.config.TunnelType),
+		TrafficEncapMode:      encapMode,
+		TrafficEncryptionMode: encryptionMode,
+		TransportIface:        o.config.TransportInterface,
 	}
 
+	wireguardConfig := &config.WireGuardConfig{
+		Port: o.config.WireGuard.Port,
+	}
 	routeClient, err := route.NewClient(serviceCIDRNet, networkConfig, o.config.NoSNAT)
 	if err != nil {
 		return fmt.Errorf("error creating route client: %v", err)
@@ -149,6 +157,7 @@ func run(o *Options) error {
 		serviceCIDRNet,
 		serviceCIDRNetv6,
 		networkConfig,
+		wireguardConfig,
 		networkReadyCh,
 		stopCh,
 		features.DefaultFeatureGate.Enabled(features.AntreaProxy))
@@ -166,7 +175,8 @@ func run(o *Options) error {
 		routeClient,
 		ifaceStore,
 		networkConfig,
-		nodeConfig)
+		nodeConfig,
+		agentInitializer.GetWireGuardClient())
 
 	var proxier proxy.Proxier
 	if features.DefaultFeatureGate.Enabled(features.AntreaProxy) {
diff --git a/cmd/antrea-agent/config.go b/cmd/antrea-agent/config.go
index d3c3a0c25..4ae77758b 100644
--- a/cmd/antrea-agent/config.go
+++ b/cmd/antrea-agent/config.go
@@ -89,11 +89,19 @@ type AgentConfig struct {
 	// --service-cluster-ip-range. When AntreaProxy is enabled, this parameter is not needed.
 	// No default value for this field.
 	ServiceCIDRv6 string `yaml:"serviceCIDRv6,omitempty"`
-	// Whether or not to enable IPSec (ESP) encryption for Pod traffic across Nodes. Antrea uses Preshared Key (PSK) for IKE
-	// authentication. When IPSec tunnel is enabled, the PSK value must be passed to Antrea Agent
-	// through an environment variable: ANTREA_IPSEC_PSK.
-	// Defaults to false.
+	// Deprecated. Use TrafficEncryptionMode instead.
 	EnableIPSecTunnel bool `yaml:"enableIPSecTunnel,omitempty"`
+	// Determines how tunnel traffic is encrypted.
+	// It has the following options:
+	// - none (default): Inter-node Pod traffic will not be encrypted.
+	// - ipsec:          Enable IPSec (ESP) encryption for Pod traffic across Nodes. Antrea uses
+	//                   Preshared Key (PSK) for IKE authentication. When IPSec tunnel is enabled,
+	//                   the PSK value must be passed to Antrea Agent through an environment
+	//                   variable: ANTREA_IPSEC_PSK.
+	// - wireguard:      Enable WireGuard for tunnel traffic encryption.
+	TrafficEncryptionMode string `yaml:"trafficEncryptionMode,omitempty"`
+	// WireGuerd related configurations.
+	WireGuard WireGuardConfig `yaml:"wireGuard"`
 	// APIPort is the port for the antrea-agent APIServer to serve on.
 	// Defaults to 10350.
 	APIPort int `yaml:"apiPort,omitempty"`
@@ -155,3 +163,8 @@ type AgentConfig struct {
 	// The interface configured with Node IP is used if this parameter is not set.
 	TransportInterface string `yaml:"transportInterface,omitempty"`
 }
+
+type WireGuardConfig struct {
+	// The port for the WireGuard to receive traffic. Defaults to 51820.
+	Port int `yaml:"port,omitempty"`
+}
diff --git a/cmd/antrea-agent/options.go b/cmd/antrea-agent/options.go
index b6a28e02a..7c4f0973b 100644
--- a/cmd/antrea-agent/options.go
+++ b/cmd/antrea-agent/options.go
@@ -116,6 +116,10 @@ func (o *Options) validate(args []string) error {
 		o.config.TunnelType != ovsconfig.GRETunnel && o.config.TunnelType != ovsconfig.STTTunnel {
 		return fmt.Errorf("tunnel type %s is invalid", o.config.TunnelType)
 	}
+	ok, encryptionMode := config.GetTrafficEncryptionModeFromStr(o.config.TrafficEncryptionMode)
+	if !ok {
+		return fmt.Errorf("TrafficEncryptionMode %s is unknown", o.config.TrafficEncryptionMode)
+	}
 	if o.config.OVSDatapathType != string(ovsconfig.OVSDatapathSystem) && o.config.OVSDatapathType != string(ovsconfig.OVSDatapathNetdev) {
 		return fmt.Errorf("OVS datapath type %s is not supported", o.config.OVSDatapathType)
 	}
@@ -134,8 +138,8 @@ func (o *Options) validate(args []string) error {
 		if !features.DefaultFeatureGate.Enabled(features.AntreaProxy) {
 			return fmt.Errorf("TrafficEncapMode %s requires AntreaProxy to be enabled", o.config.TrafficEncapMode)
 		}
-		if o.config.EnableIPSecTunnel {
-			return fmt.Errorf("IPsec tunnel may only be enabled in %s mode", config.TrafficEncapModeEncap)
+		if encryptionMode != config.TrafficEncryptionModeNone {
+			return fmt.Errorf("TrafficEncryptionMode %s may only be enabled in %s mode", encryptionMode, config.TrafficEncapModeEncap)
 		}
 	}
 	if o.config.NoSNAT && !(encapMode == config.TrafficEncapModeNoEncap || encapMode == config.TrafficEncapModeNetworkPolicyOnly) {
@@ -180,6 +184,9 @@ func (o *Options) setDefaults() {
 	if o.config.TrafficEncapMode == "" {
 		o.config.TrafficEncapMode = config.TrafficEncapModeEncap.String()
 	}
+	if o.config.TrafficEncryptionMode == "" {
+		o.config.TrafficEncryptionMode = config.TrafficEncryptionModeNone.String()
+	}
 	if o.config.TunnelType == "" {
 		o.config.TunnelType = defaultTunnelType
 	}
@@ -192,10 +199,12 @@ func (o *Options) setDefaults() {
 	if o.config.APIPort == 0 {
 		o.config.APIPort = apis.AntreaAgentAPIPort
 	}
-
 	if o.config.ClusterMembershipPort == 0 {
 		o.config.ClusterMembershipPort = apis.AntreaAgentClusterMembershipPort
 	}
+	if o.config.WireGuard.Port == 0 {
+		o.config.WireGuard.Port = apis.WireGuardListenPort
+	}
 
 	if features.DefaultFeatureGate.Enabled(features.FlowExporter) {
 		if o.config.FlowCollectorAddr == "" {
diff --git a/cmd/antrea-agent/options_windows.go b/cmd/antrea-agent/options_windows.go
index e0b8970d6..905672134 100644
--- a/cmd/antrea-agent/options_windows.go
+++ b/cmd/antrea-agent/options_windows.go
@@ -49,10 +49,10 @@ func (o *Options) checkUnsupportedFeatures() error {
 	if o.config.TunnelType == ovsconfig.GRETunnel {
 		unsupported = append(unsupported, "TunnelType: "+o.config.TunnelType)
 	}
-	if o.config.EnableIPSecTunnel {
-		unsupported = append(unsupported, "IPsecTunnel")
+	_, encryptionMode := config.GetTrafficEncryptionModeFromStr(o.config.TrafficEncryptionMode)
+	if encryptionMode != config.TrafficEncryptionModeNone {
+		unsupported = append(unsupported, "TrafficEncryptionMode: "+encryptionMode.String())
 	}
-
 	if unsupported != nil {
 		return fmt.Errorf("unsupported features on Windows: {%s}", strings.Join(unsupported, ", "))
 	}
diff --git a/docs/antrea-network-policy.md b/docs/antrea-network-policy.md
index 3622ba1f2..332daaeb8 100644
--- a/docs/antrea-network-policy.md
+++ b/docs/antrea-network-policy.md
@@ -429,14 +429,18 @@ to a separate file (`/var/log/antrea/networkpolicy/np.log`) on the Node on
 which the rule is applied. These log files can then be retrieved for further
 analysis. By default, rules are not logged. The example policy logs all
 traffic that matches the "DropToThirdParty" egress rule, while the rule
-"AllowFromFrontend" is not logged. The rules are logged in the following
-format:
+"AllowFromFrontend" is not logged. Specifically for drop and reject rules,
+deduplication is applied to simplify multiple logs. Duplication buffer length is set as 1 second.
+The rules are logged in the following format:
 
 ```text
-    <yyyy/mm/dd> <time> <ovs-table-name> <antrea-native-policy-reference> <action> <openflow-priority> SRC: <source-ip> DEST: <destination-ip> <packet-length> <protocol>
+    <yyyy/mm/dd> <time> <ovs-table-name> <antrea-native-policy-reference> <action> <openflow-priority> <source-ip> <destination-ip> <packet-length> <protocol>
+    Deduplication:
+    <yyyy/mm/dd> <time> <ovs-table-name> <antrea-native-policy-reference> <action> <openflow-priority> <source-ip> <destination-ip> <packet-length> <protocol> [<num of packets> packets in <duplicate duration>]
 
     Example:
-    2020/11/02 22:21:21.148395 AntreaPolicyAppTierIngressRule AntreaNetworkPolicy:default/test-anp Allow 61800 SRC: 10.0.0.4 DEST: 10.0.0.5 60 TCP
+    2020/11/02 22:21:21.148395 AntreaPolicyAppTierIngressRule AntreaNetworkPolicy:default/test-anp Allow 61800 10.0.0.4 10.0.0.5 60 TCP
+    2021/06/24 23:56:41.346165 AntreaPolicyEgressRule AntreaNetworkPolicy:default/test-anp Drop 44900 10.0.0.5 10.0.0.4 60 TCP [3 packets in 1.011379442s]
 ```
 
 **`appliedTo` per rule**: A ClusterNetworkPolicy ingress or egress rule may
diff --git a/go.mod b/go.mod
index 68bb77e49..cdf6af768 100644
--- a/go.mod
+++ b/go.mod
@@ -42,15 +42,16 @@ require (
 	github.com/streamrail/concurrent-map v0.0.0-20160823150647-8bf1e9bacbf6 // indirect
 	github.com/stretchr/testify v1.7.0
 	github.com/ti-mo/conntrack v0.3.0
-	github.com/vishvananda/netlink v1.1.0
+	github.com/vishvananda/netlink v1.1.1-0.20210510164352-d17758a128bf
 	github.com/vmware/go-ipfix v0.5.7
-	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
+	golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e
 	golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6
 	golang.org/x/mod v0.4.2
-	golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
+	golang.org/x/net v0.0.0-20210504132125-bbd867fde50d
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/sys v0.0.0-20210510120138-977fb7262007
 	golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
+	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5
 	google.golang.org/grpc v1.27.1
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 	gopkg.in/yaml.v2 v2.4.0
@@ -102,13 +103,15 @@ require (
 	github.com/hashicorp/golang-lru v0.5.1 // indirect
 	github.com/imdario/mergo v0.3.5 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
 	github.com/json-iterator/go v1.1.10 // indirect
 	github.com/mailru/easyjson v0.7.0 // indirect
 	github.com/mattn/go-colorable v0.1.2 // indirect
 	github.com/mattn/go-isatty v0.0.10 // indirect
 	github.com/mattn/go-runewidth v0.0.7 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
-	github.com/mdlayher/netlink v1.1.0 // indirect
+	github.com/mdlayher/genetlink v1.0.0 // indirect
+	github.com/mdlayher/netlink v1.4.0 // indirect
 	github.com/moby/spdystream v0.2.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.1 // indirect
@@ -123,7 +126,7 @@ require (
 	github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 // indirect
 	github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
 	github.com/ti-mo/netfilter v0.3.1 // indirect
-	github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
+	github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
 	go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 // indirect
 	go.opencensus.io v0.22.3 // indirect
 	go.uber.org/atomic v1.4.0 // indirect
@@ -131,8 +134,9 @@ require (
 	go.uber.org/zap v1.10.0 // indirect
 	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
 	golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
-	golang.org/x/text v0.3.4 // indirect
+	golang.org/x/text v0.3.6 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+	golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b // indirect
 	google.golang.org/appengine v1.6.5 // indirect
 	google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
 	google.golang.org/protobuf v1.27.1 // indirect
diff --git a/go.sum b/go.sum
index f00e50bec..00f7c40b6 100644
--- a/go.sum
+++ b/go.sum
@@ -302,6 +302,7 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -378,9 +379,16 @@ github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6t
 github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
 github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
+github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
 github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
-github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
 github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
+github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
+github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
+github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
+github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
+github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
+github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
@@ -436,14 +444,26 @@ github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vq
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
+github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
+github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
+github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
 github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
 github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
-github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
 github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
+github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
+github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
+github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
+github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
+github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
+github.com/mdlayher/netlink v1.4.0 h1:n3ARR+Fm0dDv37dj5wSWZXDKcy+U0zwcXS3zKMnSiT0=
+github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs=
 github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
+github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -609,11 +629,13 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
 github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
 github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
-github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
 github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netlink v1.1.1-0.20210510164352-d17758a128bf h1:JMdq3oWN6LQKfRpwVfjwuaZcPN4vGdVBzvOU2mzUXd8=
+github.com/vishvananda/netlink v1.1.1-0.20210510164352-d17758a128bf/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
 github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
-github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
 github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns=
+github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
 github.com/vmware/go-ipfix v0.5.7 h1:twv8mrXwlXhlWdixogGmyKwLia/G7hsZM46xd8aO+rc=
 github.com/vmware/go-ipfix v0.5.7/go.mod h1:yzbG1rv+yJ8GeMrRm+MDhOV3akygNZUHLhC1pDoD2AY=
 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
@@ -662,8 +684,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI=
+golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -734,11 +757,17 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
+golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -794,18 +823,30 @@ golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -817,8 +858,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -875,6 +917,10 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b h1:XDLXhn7ryprJVo+Lpkiib6CIuXE2031GDwtfEm7vLjI=
+golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
+golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5 h1:LpEwXnbN4q2EIPkqbG9KHBUrducJYDOOdL+eMcJAlFo=
+golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
diff --git a/hack/build-whereabouts.sh b/hack/build-whereabouts.sh
new file mode 100755
index 000000000..2343281c6
--- /dev/null
+++ b/hack/build-whereabouts.sh
@@ -0,0 +1,117 @@
+#!/usr/bin/env bash
+
+# Copyright 2021 Antrea Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -eo pipefail
+
+function echoerr {
+    >&2 echo "$@"
+}
+
+_usage="Usage: $0 --out <OUT_DIR> --ref <WHEREABOUTS_REF>
+Build the whereabouts binaries and get them ready for upload to downloads.antrea.io
+        --out <OUT_DIR>                Directory where binaries should be generated
+        --ref <WHEREABOUTS_REF>        Whereabouts ref to use (tag, sha)"
+
+function print_usage {
+    echoerr "$_usage"
+}
+
+function print_help {
+    echoerr "Try '$0 --help' for more information."
+}
+
+OUT_DIR=""
+WHEREABOUTS_REF=""
+
+while [[ $# -gt 0 ]]
+do
+key="$1"
+
+case $key in
+    --out)
+    OUT_DIR="$2"
+    shift 2
+    ;;
+    --ref)
+    WHEREABOUTS_REF="$2"
+    shift 2
+    ;;
+    -h|--help)
+    print_usage
+    exit 0
+    ;;
+    *)    # unknown option
+    echoerr "Unknown option $1"
+    exit 1
+    ;;
+esac
+done
+
+if [ "$OUT_DIR" == "" ]; then
+    echoerr "--out is required"
+    print_help
+    exit 1
+fi
+
+if [ "$WHEREABOUTS_REF" == "" ]; then
+    echoerr "--ref is required"
+    print_help
+    exit 1
+fi
+
+mkdir -p "$OUT_DIR"
+OUT_DIR=$(cd "$OUT_DIR" && pwd)
+FILES_DIR="$OUT_DIR/whereabouts/$WHEREABOUTS_REF"
+mkdir -p "$FILES_DIR"
+
+pushd $OUT_DIR > /dev/null
+
+WORKDIR=$(mktemp -d $OUT_DIR/workdir.XXXXXXXX)
+git clone --depth 1 --branch "$WHEREABOUTS_REF" https://github.com/k8snetworkplumbingwg/whereabouts.git $WORKDIR
+
+pushd $WORKDIR > /dev/null
+
+WHEREABOUTS_BUILDS=(
+    "linux amd64"
+    "linux arm64"
+    "linux arm"
+)
+
+FILES=""
+for build in "${WHEREABOUTS_BUILDS[@]}"; do
+    args=($build)
+    os="${args[0]}"
+    arch="${args[1]}"
+    name="whereabouts-${os}-${arch}"
+    echoerr "Building ${name} ..."
+
+    GOOS=$os GOARCH=$arch ./hack/build-go.sh
+    mkdir $name
+    mv bin/whereabouts $name/
+    cp LICENSE $name/
+    tar -zcvf "${FILES_DIR}/${name}.tgz" $name
+    FILES="$FILES $name.tgz"
+done
+
+
+popd > /dev/null
+
+rm -rf $WORKDIR
+
+popd > /dev/null
+
+echoerr "Files created under $FILES_DIR: $FILES"
+echoerr "You can now upload $FILES_DIR to downloads.antrea.io"
diff --git a/hack/generate-manifest.sh b/hack/generate-manifest.sh
index 1ecdf6874..6a779c635 100755
--- a/hack/generate-manifest.sh
+++ b/hack/generate-manifest.sh
@@ -44,6 +44,8 @@ Generate a YAML manifest for Antrea using Kustomize and print it to stdout.
         --custom-adm-controller       Generates a manifest with custom Antrea admission controller to validate/mutate resources.
         --hw-offload                  Generates a manifest with hw-offload enabled in the antrea-ovs container.
         --sriov                       Generates a manifest which enables use of Kubelet API for SR-IOV device info.
+        --wireguard-go                Generate a manifest with WireGuard (golang implementation) encryption enabled.
+                                      This option will work only for Kind clusters (when using '--kind').
         --help, -h                    Print this message and exit
 
 In 'release' mode, environment variables IMG_NAME and IMG_TAG must be set.
@@ -84,6 +86,7 @@ SIMULATOR=false
 CUSTOM_ADM_CONTROLLER=false
 HW_OFFLOAD=false
 SRIOV=false
+WIREGUARD_GO=false
 
 while [[ $# -gt 0 ]]
 do
@@ -170,6 +173,10 @@ case $key in
     --sriov)
     SRIOV=true
     shift
+    ;;   
+    --wireguard-go)
+    WIREGUARD_GO=true
+    shift
     ;;
     -h|--help)
     print_usage
@@ -229,6 +236,18 @@ if [[ "$ENCAP_MODE" != "" ]] && [[ "$ENCAP_MODE" != "encap" ]] && ! $PROXY; then
     exit 1
 fi
 
+if "$WIREGUARD_GO" && "$IPSEC"; then
+    echoerr "Cannot use '--wireguard-go' together with '--ipsec'"
+    print_help
+    exit 1
+fi
+
+if "$WIREGUARD_GO" && ! "$KIND"; then
+    echoerr "--wireguard-go works only for Kind clusters"
+    print_help
+    exit 1
+fi
+
 THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
 
 source $THIS_DIR/verify-kustomize.sh
@@ -260,11 +279,15 @@ if $KIND; then
 fi
 
 if $IPSEC; then
-    sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*enableIPSecTunnel[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/enableIPSecTunnel: true/" antrea-agent.conf
+    sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*trafficEncryptionMode[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/trafficEncryptionMode: ipsec/" antrea-agent.conf
     # change the tunnel type to GRE which works better with IPSec encryption than other types.
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*tunnelType[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/tunnelType: gre/" antrea-agent.conf
 fi
 
+if $WIREGUARD_GO; then
+    sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*trafficEncryptionMode[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/trafficEncryptionMode: wireguard/" antrea-agent.conf
+fi
+
 if $ALLFEATURES; then
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*AntreaPolicy[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  AntreaPolicy: true/" antrea-agent.conf
     sed -i.bak -E "s/^[[:space:]]*#[[:space:]]*FlowExporter[[:space:]]*:[[:space:]]*[a-z]+[[:space:]]*$/  FlowExporter: true/" antrea-agent.conf
@@ -410,6 +433,11 @@ if $KIND; then
     # change initContainer script and remove SYS_MODULE capability
     $KUSTOMIZE edit add patch --path installCni.yml
 
+    # inject the wireguard-go container to run WireGuard in userspace
+    if $WIREGUARD_GO; then
+        $KUSTOMIZE edit add patch --path wireguardGo.yml
+    fi
+
     if $ON_DELETE; then
         $KUSTOMIZE edit add patch --path onDeleteUpdateStrategy.yml
     fi
diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go
index 75cba398c..b766bb451 100644
--- a/pkg/agent/agent.go
+++ b/pkg/agent/agent.go
@@ -42,6 +42,7 @@ import (
 	"antrea.io/antrea/pkg/agent/route"
 	"antrea.io/antrea/pkg/agent/types"
 	"antrea.io/antrea/pkg/agent/util"
+	"antrea.io/antrea/pkg/agent/wireguard"
 	"antrea.io/antrea/pkg/features"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
 	"antrea.io/antrea/pkg/ovs/ovsctl"
@@ -72,6 +73,7 @@ type Initializer struct {
 	ovsBridgeClient ovsconfig.OVSBridgeClient
 	ofClient        openflow.Client
 	routeClient     route.Interface
+	wireGuardClient wireguard.Interface
 	ifaceStore      interfacestore.InterfaceStore
 	ovsBridge       string
 	hostGateway     string // name of gateway port on the OVS bridge
@@ -80,6 +82,7 @@ type Initializer struct {
 	serviceCIDRv6   *net.IPNet // K8s Service ClusterIP CIDR in IPv6
 	networkConfig   *config.NetworkConfig
 	nodeConfig      *config.NodeConfig
+	wireGuardConfig *config.WireGuardConfig
 	enableProxy     bool
 	// networkReadyCh should be closed once the Node's network is ready.
 	// The CNI server will wait for it before handling any CNI Add requests.
@@ -99,6 +102,7 @@ func NewInitializer(
 	serviceCIDR *net.IPNet,
 	serviceCIDRv6 *net.IPNet,
 	networkConfig *config.NetworkConfig,
+	wireGuardConfig *config.WireGuardConfig,
 	networkReadyCh chan<- struct{},
 	stopCh <-chan struct{},
 	enableProxy bool) *Initializer {
@@ -114,6 +118,7 @@ func NewInitializer(
 		serviceCIDR:     serviceCIDR,
 		serviceCIDRv6:   serviceCIDRv6,
 		networkConfig:   networkConfig,
+		wireGuardConfig: wireGuardConfig,
 		networkReadyCh:  networkReadyCh,
 		stopCh:          stopCh,
 		enableProxy:     enableProxy,
@@ -125,6 +130,11 @@ func (i *Initializer) GetNodeConfig() *config.NodeConfig {
 	return i.nodeConfig
 }
 
+// GetNodeConfig returns the NodeConfig.
+func (i *Initializer) GetWireGuardClient() wireguard.Interface {
+	return i.wireGuardClient
+}
+
 // setupOVSBridge sets up the OVS bridge and create host gateway interface and tunnel port
 func (i *Initializer) setupOVSBridge() error {
 	if err := i.ovsBridgeClient.Create(); err != nil {
@@ -259,8 +269,15 @@ func (i *Initializer) Initialize() error {
 		return err
 	}
 
-	if err := i.initializeIPSec(); err != nil {
-		return err
+	switch i.networkConfig.TrafficEncryptionMode {
+	case config.TrafficEncryptionModeIPSec:
+		if err := i.initializeIPSec(); err != nil {
+			return err
+		}
+	case config.TrafficEncryptionModeWireGuard:
+		if err := i.initializeWireGuard(); err != nil {
+			return err
+		}
 	}
 
 	if err := i.prepareHostNetwork(); err != nil {
@@ -337,7 +354,7 @@ func (i *Initializer) initOpenFlowPipeline() error {
 	roundInfo := getRoundInfo(i.ovsBridgeClient)
 
 	// Set up all basic flows.
-	ofConnCh, err := i.ofClient.Initialize(roundInfo, i.nodeConfig, i.networkConfig.TrafficEncapMode)
+	ofConnCh, err := i.ofClient.Initialize(roundInfo, i.nodeConfig, i.networkConfig)
 	if err != nil {
 		klog.Errorf("Failed to initialize openflow client: %v", err)
 		return err
@@ -737,7 +754,10 @@ func (i *Initializer) initNodeLocalConfig() error {
 		NodeIPv6Addr:          nodeIPv6Addr,
 		NodeTransportIPv4Addr: transportIPv4Addr,
 		NodeTransportIPv6Addr: transportIPv6Addr,
-		UplinkNetConfig:       new(config.AdapterNetConfig)}
+		UplinkNetConfig:       new(config.AdapterNetConfig),
+		NodeLocalInterfaceMTU: localIntf.MTU,
+		WireGuardConfig:       i.wireGuardConfig,
+	}
 
 	mtu, err := i.getNodeMTU(localIntf)
 	if err != nil {
@@ -797,10 +817,6 @@ func (i *Initializer) initNodeLocalConfig() error {
 
 // initializeIPSec checks if preconditions are met for using IPsec and reads the IPsec PSK value.
 func (i *Initializer) initializeIPSec() error {
-	if !i.networkConfig.EnableIPSecTunnel {
-		return nil
-	}
-
 	// At the time the agent is initialized and this code is executed, the
 	// OVS daemons are already running given that we have successfully
 	// connected to OVSDB. Given that the start_ovs script deletes existing
@@ -830,6 +846,18 @@ func (i *Initializer) initializeIPSec() error {
 	return nil
 }
 
+// initializeWireguard checks if preconditions are met for using WireGuard and initializes WireGuard client or cleans up.
+func (i *Initializer) initializeWireGuard() error {
+	i.wireGuardConfig.MTU = i.nodeConfig.NodeLocalInterfaceMTU - config.WireGuardOverhead
+	wgClient, err := wireguard.New(i.client, i.nodeConfig, i.wireGuardConfig)
+	if err != nil {
+		return err
+	}
+
+	i.wireGuardClient = wgClient
+	return i.wireGuardClient.Init()
+}
+
 // readIPSecPSK reads the IPsec PSK value from environment variable ANTREA_IPSEC_PSK
 func (i *Initializer) readIPSecPSK() error {
 	i.networkConfig.IPSecPSK = os.Getenv(ipsecPSKEnvKey)
@@ -914,7 +942,7 @@ func (i *Initializer) getNodeMTU(localIntf *net.Interface) (int, error) {
 			mtu -= config.IPv6ExtraOverhead
 		}
 	}
-	if i.networkConfig.EnableIPSecTunnel {
+	if i.networkConfig.TrafficEncryptionMode == config.TrafficEncryptionModeIPSec {
 		mtu -= config.IPSecESPOverhead
 	}
 	return mtu, nil
diff --git a/pkg/agent/config/node_config.go b/pkg/agent/config/node_config.go
index 2089d359f..d5f8e0499 100644
--- a/pkg/agent/config/node_config.go
+++ b/pkg/agent/config/node_config.go
@@ -34,9 +34,10 @@ const (
 )
 
 const (
-	VXLANOverhead  = 50
-	GeneveOverhead = 50
-	GREOverhead    = 38
+	VXLANOverhead     = 50
+	GeneveOverhead    = 50
+	GREOverhead       = 38
+	WireGuardOverhead = 80
 	// IPsec ESP can add a maximum of 38 bytes to the packet including the ESP
 	// header and trailer.
 	IPSecESPOverhead  = 38
@@ -68,6 +69,17 @@ type AdapterNetConfig struct {
 	Routes     []interface{}
 }
 
+type WireGuardConfig struct {
+	// Name is the name of WireGurad interface. e.g. antrea-wg0.
+	Name string
+	// LinkIndex is the link index of WireGuard interface.
+	LinkIndex int
+	// Port is the port for the WireGuard to receive traffic.
+	Port int
+	// The MTU of WireGuard interface.
+	MTU int
+}
+
 // Local Node configurations retrieved from K8s API or host networking state.
 type NodeConfig struct {
 	// The Node's name used in Kubernetes.
@@ -91,6 +103,8 @@ type NodeConfig struct {
 	NodeTransportIPv4Addr *net.IPNet
 	// The IPv6 address on the Node's transport interface. It is used for tunneling or routing the Pod traffic across Nodes.
 	NodeTransportIPv6Addr *net.IPNet
+	// The original MTU of Node's local interface which has the K8s Node IP.
+	NodeLocalInterfaceMTU int
 	// Set either via defaultMTU config in antrea.yaml or auto discovered.
 	// Auto discovery will use MTU value of the Node's primary interface.
 	// For Encap and Hybrid mode, Node MTU will be adjusted to account for encap header.
@@ -99,6 +113,8 @@ type NodeConfig struct {
 	GatewayConfig *GatewayConfig
 	// The config of the OVS bridge uplink interface. Only for Windows Node.
 	UplinkNetConfig *AdapterNetConfig
+	// The config of the WireGuard interface.
+	WireGuardConfig *WireGuardConfig
 }
 
 func (n *NodeConfig) String() string {
@@ -108,11 +124,11 @@ func (n *NodeConfig) String() string {
 
 // User provided network configuration parameters.
 type NetworkConfig struct {
-	TrafficEncapMode  TrafficEncapModeType
-	TunnelType        ovsconfig.TunnelType
-	EnableIPSecTunnel bool
-	IPSecPSK          string
-	TransportIface    string
+	TrafficEncapMode      TrafficEncapModeType
+	TunnelType            ovsconfig.TunnelType
+	TrafficEncryptionMode TrafficEncryptionModeType
+	IPSecPSK              string
+	TransportIface        string
 }
 
 // IsIPv4Enabled returns true if the cluster network supports IPv4.
@@ -126,3 +142,16 @@ func IsIPv6Enabled(nodeConfig *NodeConfig, trafficEncapMode TrafficEncapModeType
 	return nodeConfig.PodIPv6CIDR != nil ||
 		(trafficEncapMode.IsNetworkPolicyOnly() && nodeConfig.NodeIPv6Addr != nil)
 }
+
+// NeedsTunnelToPeer returns true if Pod traffic to peer Node needs to be encapsulated by OVS tunneling.
+func (nc *NetworkConfig) NeedsTunnelToPeer(peerIP net.IP, localIP *net.IPNet) bool {
+	if nc.TrafficEncryptionMode == TrafficEncryptionModeWireGuard {
+		return false
+	}
+	return nc.TrafficEncapMode == TrafficEncapModeEncap || (nc.TrafficEncapMode == TrafficEncapModeHybrid && !localIP.Contains(peerIP))
+}
+
+// NeedsDirectRoutingToPeer returns true if Pod traffic to peer Node needs a direct route installed to the routing table.
+func (nc *NetworkConfig) NeedsDirectRoutingToPeer(peerIP net.IP, localIP *net.IPNet) bool {
+	return (nc.TrafficEncapMode == TrafficEncapModeNoEncap || nc.TrafficEncapMode == TrafficEncapModeHybrid) && localIP.Contains(peerIP)
+}
diff --git a/pkg/agent/config/traffic_encap_mode.go b/pkg/agent/config/traffic_encap_mode.go
index 878d53c50..430a621bf 100644
--- a/pkg/agent/config/traffic_encap_mode.go
+++ b/pkg/agent/config/traffic_encap_mode.go
@@ -15,7 +15,6 @@
 package config
 
 import (
-	"net"
 	"strings"
 )
 
@@ -77,13 +76,3 @@ func (m TrafficEncapModeType) SupportsNoEncap() bool {
 func (m TrafficEncapModeType) SupportsEncap() bool {
 	return m == TrafficEncapModeEncap || m == TrafficEncapModeHybrid
 }
-
-// NeedsEncapToPeer returns true if Pod traffic to peer Node needs to be encapsulated.
-func (m TrafficEncapModeType) NeedsEncapToPeer(peerIP net.IP, localIP *net.IPNet) bool {
-	return (m == TrafficEncapModeEncap) || (m == TrafficEncapModeHybrid && !localIP.Contains(peerIP))
-}
-
-// NeedsDirectRoutingToPeer returns true if Pod traffic to peer Node needs a direct route installed to the routing table.
-func (m TrafficEncapModeType) NeedsDirectRoutingToPeer(peerIP net.IP, localIP *net.IPNet) bool {
-	return (m == TrafficEncapModeNoEncap || m == TrafficEncapModeHybrid) && localIP.Contains(peerIP)
-}
diff --git a/pkg/agent/config/traffic_encryption_mode.go b/pkg/agent/config/traffic_encryption_mode.go
new file mode 100644
index 000000000..e21d445a1
--- /dev/null
+++ b/pkg/agent/config/traffic_encryption_mode.go
@@ -0,0 +1,60 @@
+// Copyright 2021 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+	"strings"
+)
+
+type TrafficEncryptionModeType int
+
+const (
+	TrafficEncryptionModeNone TrafficEncryptionModeType = iota
+	TrafficEncryptionModeIPSec
+	TrafficEncryptionModeWireGuard
+	TrafficEncryptionModeInvalid = -1
+)
+
+var (
+	encryptionModeStrs = [...]string{
+		"None",
+		"IPsec",
+		"WireGuard",
+	}
+)
+
+// GetTrafficEncryptionModeFromStr returns true and TrafficEncryptionModeType corresponding to input string.
+// Otherwise, false and undefined value is returned
+func GetTrafficEncryptionModeFromStr(str string) (bool, TrafficEncryptionModeType) {
+	for idx, ms := range encryptionModeStrs {
+		if strings.EqualFold(ms, str) {
+			return true, TrafficEncryptionModeType(idx)
+		}
+	}
+	return false, TrafficEncryptionModeInvalid
+}
+
+func GetTrafficEncryptionModes() []TrafficEncryptionModeType {
+	return []TrafficEncryptionModeType{
+		TrafficEncryptionModeNone,
+		TrafficEncryptionModeIPSec,
+		TrafficEncryptionModeWireGuard,
+	}
+}
+
+// String returns value in string.
+func (m TrafficEncryptionModeType) String() string {
+	return encryptionModeStrs[m]
+}
diff --git a/pkg/agent/controller/networkpolicy/audit_logging.go b/pkg/agent/controller/networkpolicy/audit_logging.go
index 5920745b0..1c003ede0 100644
--- a/pkg/agent/controller/networkpolicy/audit_logging.go
+++ b/pkg/agent/controller/networkpolicy/audit_logging.go
@@ -111,7 +111,7 @@ func (l *AntreaPolicyLogger) updateLogKey(logMsg string, bufferLength time.Durat
 // LogDedupPacket logs information in ob based on disposition and duplication conditions.
 func (l *AntreaPolicyLogger) LogDedupPacket(ob *logInfo) {
 	// Deduplicate non-Allow packet log.
-	logMsg := fmt.Sprintf("%s %s %s %s SRC: %s DEST: %s %d %s", ob.tableName, ob.npRef, ob.disposition, ob.ofPriority, ob.srcIP, ob.destIP, ob.pktLength, ob.protocolStr)
+	logMsg := fmt.Sprintf("%s %s %s %s %s %s %d %s", ob.tableName, ob.npRef, ob.disposition, ob.ofPriority, ob.srcIP, ob.destIP, ob.pktLength, ob.protocolStr)
 	if ob.disposition == openflow.DispositionToString[openflow.DispositionAllow] {
 		l.anpLogger.Printf(logMsg)
 	} else {
diff --git a/pkg/agent/controller/networkpolicy/packetin.go b/pkg/agent/controller/networkpolicy/packetin.go
index ed4ccdf8c..6ca86c8d0 100644
--- a/pkg/agent/controller/networkpolicy/packetin.go
+++ b/pkg/agent/controller/networkpolicy/packetin.go
@@ -335,43 +335,60 @@ func (c *Controller) storeDenyConnection(pktIn *ofctrl.PacketIn) error {
 	}
 	disposition := openflow.DispositionToString[id]
 
-	// For K8s NetworkPolicy implicit drop action, we cannot get name/namespace.
-	if tableID == openflow.IngressDefaultTable {
-		denyConn.IngressNetworkPolicyType = registry.PolicyTypeK8sNetworkPolicy
-		denyConn.IngressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
-	} else if tableID == openflow.EgressDefaultTable {
-		denyConn.EgressNetworkPolicyType = registry.PolicyTypeK8sNetworkPolicy
-		denyConn.EgressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
-	} else { // Get name and namespace for Antrea Network Policy or Antrea Cluster Network Policy
-		// Set match to corresponding ingress/egress reg according to disposition
-		match = getMatch(matchers, tableID, id)
+	// Set match to corresponding ingress/egress reg according to disposition
+	match = getMatch(matchers, tableID, id)
+	if match != nil {
 		ruleID, err := getInfoInReg(match, nil)
 		if err != nil {
 			return fmt.Errorf("error when obtaining rule id from reg: %v", err)
 		}
 		policy := c.GetNetworkPolicyByRuleFlowID(ruleID)
 		rule := c.GetRuleByFlowID(ruleID)
-
 		if policy == nil || rule == nil {
-			// Default drop by K8s NetworkPolicy
 			klog.V(4).Infof("Cannot find NetworkPolicy or rule that has ruleID %v", ruleID)
-		} else {
-			if tableID == openflow.AntreaPolicyIngressRuleTable {
-				denyConn.IngressNetworkPolicyName = policy.Name
-				denyConn.IngressNetworkPolicyNamespace = policy.Namespace
-				denyConn.IngressNetworkPolicyType = flowexporter.PolicyTypeToUint8(policy.Type)
-				denyConn.IngressNetworkPolicyRuleName = rule.Name
-				denyConn.IngressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
-			} else if tableID == openflow.AntreaPolicyEgressRuleTable {
-				denyConn.EgressNetworkPolicyName = policy.Name
-				denyConn.EgressNetworkPolicyNamespace = policy.Namespace
-				denyConn.EgressNetworkPolicyType = flowexporter.PolicyTypeToUint8(policy.Type)
-				denyConn.EgressNetworkPolicyRuleName = rule.Name
-				denyConn.EgressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
-			}
+		}
+		// Get name and namespace for Antrea Network Policy or Antrea Cluster Network Policy
+		if isAntreaPolicyIngressTable(tableID) {
+			denyConn.IngressNetworkPolicyName = policy.Name
+			denyConn.IngressNetworkPolicyNamespace = policy.Namespace
+			denyConn.IngressNetworkPolicyType = flowexporter.PolicyTypeToUint8(policy.Type)
+			denyConn.IngressNetworkPolicyRuleName = rule.Name
+			denyConn.IngressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
+		} else if isAntreaPolicyEgressTable(tableID) {
+			denyConn.EgressNetworkPolicyName = policy.Name
+			denyConn.EgressNetworkPolicyNamespace = policy.Namespace
+			denyConn.EgressNetworkPolicyType = flowexporter.PolicyTypeToUint8(policy.Type)
+			denyConn.EgressNetworkPolicyRuleName = rule.Name
+			denyConn.EgressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
+		}
+	} else {
+		// For K8s NetworkPolicy implicit drop action, we cannot get name/namespace.
+		if tableID == openflow.IngressDefaultTable {
+			denyConn.IngressNetworkPolicyType = registry.PolicyTypeK8sNetworkPolicy
+			denyConn.IngressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
+		} else if tableID == openflow.EgressDefaultTable {
+			denyConn.EgressNetworkPolicyType = registry.PolicyTypeK8sNetworkPolicy
+			denyConn.EgressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
 		}
 	}
-
 	c.denyConnStore.AddOrUpdateConn(&denyConn, time.Now(), uint64(packet.IPLength))
 	return nil
 }
+
+func isAntreaPolicyIngressTable(tableID binding.TableIDType) bool {
+	for _, table := range openflow.GetAntreaPolicyIngressTables() {
+		if table == tableID {
+			return true
+		}
+	}
+	return false
+}
+
+func isAntreaPolicyEgressTable(tableID binding.TableIDType) bool {
+	for _, table := range openflow.GetAntreaPolicyEgressTables() {
+		if table == tableID {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/agent/controller/noderoute/node_route_controller.go b/pkg/agent/controller/noderoute/node_route_controller.go
index 5cd5b0941..8215d8ae6 100644
--- a/pkg/agent/controller/noderoute/node_route_controller.go
+++ b/pkg/agent/controller/noderoute/node_route_controller.go
@@ -38,6 +38,7 @@ import (
 	"antrea.io/antrea/pkg/agent/route"
 	"antrea.io/antrea/pkg/agent/types"
 	"antrea.io/antrea/pkg/agent/util"
+	"antrea.io/antrea/pkg/agent/wireguard"
 	"antrea.io/antrea/pkg/ovs/ovsconfig"
 	utilip "antrea.io/antrea/pkg/util/ip"
 	"antrea.io/antrea/pkg/util/k8s"
@@ -74,7 +75,8 @@ type Controller struct {
 	// installedNodes records routes and flows installation states of Nodes.
 	// The key is the host name of the Node, the value is the nodeRouteInfo of the Node.
 	// A node will be in the map after its flows and routes are installed successfully.
-	installedNodes cache.Indexer
+	installedNodes  cache.Indexer
+	wireGuardClient wireguard.Interface
 }
 
 // NewNodeRouteController instantiates a new Controller object which will process Node events
@@ -87,7 +89,9 @@ func NewNodeRouteController(
 	routeClient route.Interface,
 	interfaceStore interfacestore.InterfaceStore,
 	networkConfig *config.NetworkConfig,
-	nodeConfig *config.NodeConfig) *Controller {
+	nodeConfig *config.NodeConfig,
+	wireguardClient wireguard.Interface,
+) *Controller {
 	nodeInformer := informerFactory.Core().V1().Nodes()
 	controller := &Controller{
 		kubeClient:       kubeClient,
@@ -102,6 +106,7 @@ func NewNodeRouteController(
 		nodeListerSynced: nodeInformer.Informer().HasSynced,
 		queue:            workqueue.NewNamedRateLimitingQueue(workqueue.NewItemExponentialFailureRateLimiter(minRetryDelay, maxRetryDelay), "noderoute"),
 		installedNodes:   cache.NewIndexer(nodeRouteInfoKeyFunc, cache.Indexers{nodeRouteInfoPodCIDRIndexName: nodeRouteInfoPodCIDRIndexFunc}),
+		wireGuardClient:  wireguardClient,
 	}
 	nodeInformer.Informer().AddEventHandlerWithResyncPeriod(
 		cache.ResourceEventHandlerFuncs{
@@ -134,11 +139,12 @@ func nodeRouteInfoPodCIDRIndexFunc(obj interface{}) ([]string, error) {
 
 // nodeRouteInfo is the route related information extracted from corev1.Node.
 type nodeRouteInfo struct {
-	nodeName   string
-	podCIDRs   []*net.IPNet
-	nodeIPs    *utilip.DualStackIPs
-	gatewayIPs *utilip.DualStackIPs
-	nodeMAC    net.HardwareAddr
+	nodeName           string
+	podCIDRs           []*net.IPNet
+	nodeIPs            *utilip.DualStackIPs
+	gatewayIPs         *utilip.DualStackIPs
+	nodeMAC            net.HardwareAddr
+	wireGuardPublicKey string
 }
 
 // enqueueNode adds an object to the controller work queue
@@ -208,7 +214,7 @@ func (c *Controller) removeStaleTunnelPorts() error {
 	// knownInterfaces is the list of interfaces currently in the local cache.
 	knownInterfaces := c.interfaceStore.GetInterfaceKeysByType(interfacestore.TunnelInterface)
 
-	if c.networkConfig.EnableIPSecTunnel {
+	if c.networkConfig.TrafficEncryptionMode == config.TrafficEncryptionModeIPSec {
 		for _, node := range nodes {
 			interfaceConfig, found := c.interfaceStore.GetNodeTunnelInterface(node.Name)
 			if !found {
@@ -276,9 +282,30 @@ func (c *Controller) reconcile() error {
 	if err := c.removeStaleTunnelPorts(); err != nil {
 		return fmt.Errorf("error when removing stale tunnel ports: %v", err)
 	}
+	if err := c.removeStaleWireGuardPeers(); err != nil {
+		return fmt.Errorf("error when removing stale WireGuard peers: %v", err)
+	}
 	return nil
 }
 
+// removeStaleWireGuardPeers deletes stale WireGuard peers if necessary.
+func (c *Controller) removeStaleWireGuardPeers() error {
+	if c.networkConfig.TrafficEncryptionMode != config.TrafficEncryptionModeWireGuard {
+		return nil
+	}
+	nodes, err := c.nodeLister.List(labels.Everything())
+	if err != nil {
+		return fmt.Errorf("error when listing Nodes: %v", err)
+	}
+	currentPeerPublicKeys := make(map[string]string)
+	for _, n := range nodes {
+		if pubkey, ok := n.Annotations[types.NodeWireGuardPublicAnnotationKey]; ok {
+			currentPeerPublicKeys[n.Name] = pubkey
+		}
+	}
+	return c.wireGuardClient.RemoveStalePeers(currentPeerPublicKeys)
+}
+
 // Run will create defaultWorkers workers (go routines) which will process the Node events from the
 // workqueue.
 func (c *Controller) Run(stopCh <-chan struct{}) {
@@ -300,7 +327,7 @@ func (c *Controller) Run(stopCh <-chan struct{}) {
 	}
 
 	if err := c.reconcile(); err != nil {
-		klog.Errorf("Error during %s reconciliation", controllerName)
+		klog.ErrorS(err, "Error during reconciliation", "controller", controllerName)
 	}
 
 	for i := 0; i < defaultWorkers; i++ {
@@ -399,7 +426,7 @@ func (c *Controller) deleteNodeRoute(nodeName string) error {
 	}
 	c.installedNodes.Delete(obj)
 
-	if c.networkConfig.EnableIPSecTunnel {
+	if c.networkConfig.TrafficEncryptionMode == config.TrafficEncryptionModeIPSec {
 		interfaceConfig, ok := c.interfaceStore.GetNodeTunnelInterface(nodeName)
 		if !ok {
 			// Tunnel port not created for this Node.
@@ -412,6 +439,12 @@ func (c *Controller) deleteNodeRoute(nodeName string) error {
 		}
 		c.interfaceStore.DeleteInterface(interfaceConfig)
 	}
+
+	if c.networkConfig.TrafficEncryptionMode == config.TrafficEncryptionModeWireGuard {
+		if err := c.wireGuardClient.DeletePeer(nodeName); err != nil {
+			return fmt.Errorf("delete WireGuard peer %s failed: %v", nodeName, err)
+		}
+	}
 	return nil
 }
 
@@ -425,10 +458,14 @@ func (c *Controller) addNodeRoute(nodeName string, node *corev1.Node) error {
 	if err != nil {
 		return err
 	}
+	peerWireGuardPublicKey := node.Annotations[types.NodeWireGuardPublicAnnotationKey]
 
 	nrInfo, installed, _ := c.installedNodes.GetByKey(nodeName)
-	if installed && nrInfo.(*nodeRouteInfo).nodeMAC.String() == peerNodeMAC.String() && peerNodeIPs.Equal(*nrInfo.(*nodeRouteInfo).nodeIPs) {
-		// Route is already added for this Node and both Node MAC and transport IP are not changed.
+	// Route is already added for this Node and Node MAC, transport IP
+	// and WireGuard public key are not changed.
+	if installed && nrInfo.(*nodeRouteInfo).nodeMAC.String() == peerNodeMAC.String() &&
+		peerNodeIPs.Equal(*nrInfo.(*nodeRouteInfo).nodeIPs) &&
+		nrInfo.(*nodeRouteInfo).wireGuardPublicKey == peerWireGuardPublicKey {
 		return nil
 	}
 
@@ -485,7 +522,7 @@ func (c *Controller) addNodeRoute(nodeName string, node *corev1.Node) error {
 	}
 
 	var ipsecTunOFPort uint32
-	if c.networkConfig.EnableIPSecTunnel {
+	if c.networkConfig.TrafficEncryptionMode == config.TrafficEncryptionModeIPSec {
 		// Create a separate tunnel port for the Node, as OVS IPSec monitor needs to
 		// read PSK and remote IP from the Node's tunnel interface to create IPSec
 		// security policies.
@@ -500,6 +537,16 @@ func (c *Controller) addNodeRoute(nodeName string, node *corev1.Node) error {
 		ipsecTunOFPort = uint32(port)
 	}
 
+	if c.networkConfig.TrafficEncryptionMode == config.TrafficEncryptionModeWireGuard && peerWireGuardPublicKey != "" {
+		peerNodeIP := peerNodeIPs.IPv4
+		if peerNodeIP == nil {
+			peerNodeIP = peerNodeIPs.IPv6
+		}
+		if err := c.wireGuardClient.UpdatePeer(nodeName, peerWireGuardPublicKey, peerNodeIP, peerPodCIDRs); err != nil {
+			return err
+		}
+	}
+
 	if err = c.ofClient.InstallNodeFlows(
 		nodeName,
 		peerConfigs,
@@ -523,12 +570,14 @@ func (c *Controller) addNodeRoute(nodeName string, node *corev1.Node) error {
 			peerGatewayIPs.IPv4 = peerGatewayIP
 		}
 	}
+
 	c.installedNodes.Add(&nodeRouteInfo{
-		nodeName:   nodeName,
-		podCIDRs:   peerPodCIDRs,
-		nodeIPs:    peerNodeIPs,
-		gatewayIPs: peerGatewayIPs,
-		nodeMAC:    peerNodeMAC,
+		nodeName:           nodeName,
+		podCIDRs:           peerPodCIDRs,
+		nodeIPs:            peerNodeIPs,
+		gatewayIPs:         peerGatewayIPs,
+		nodeMAC:            peerNodeMAC,
+		wireGuardPublicKey: peerWireGuardPublicKey,
 	})
 
 	return err
@@ -551,19 +600,26 @@ func getPodCIDRsOnNode(node *corev1.Node) []string {
 // tunnel does not exist, and returns the ofport number.
 func (c *Controller) createIPSecTunnelPort(nodeName string, nodeIP net.IP) (int32, error) {
 	portName := util.GenerateNodeTunnelInterfaceName(nodeName)
-	interfaceConfig, ok := c.interfaceStore.GetNodeTunnelInterface(nodeName)
+	interfaceConfig, exists := c.interfaceStore.GetNodeTunnelInterface(nodeName)
 	// check if Node IP, PSK, or tunnel type changes. This can
 	// happen if removeStaleTunnelPorts fails to remove a "stale"
 	// tunnel port for which the configuration has changed, return error to requeue the Node.
-	if ok {
+	if exists {
 		if !c.compareInterfaceConfig(interfaceConfig, nodeIP, portName) {
-			return 0, fmt.Errorf("IPSec tunnel interface config doesn't match cached one, stale IPSec tunnel port %s", interfaceConfig.InterfaceName)
-		}
-		klog.V(2).InfoS("Found cached IPSec tunnel interface", "interfaceName", interfaceConfig.InterfaceName, "port", interfaceConfig.OFPort, "nodeName", nodeName)
-		if interfaceConfig.OFPort != 0 {
-			return interfaceConfig.OFPort, nil
+			klog.InfoS("IPSec tunnel interface config doesn't match the cached one, deleting the stale IPSec tunnel port", "node", nodeName, "interface", interfaceConfig.InterfaceName)
+			if err := c.ovsBridgeClient.DeletePort(interfaceConfig.PortUUID); err != nil {
+				return 0, fmt.Errorf("fail to delete the stale IPSec tunnel port %s: %v", interfaceConfig.InterfaceName, err)
+			}
+			c.interfaceStore.DeleteInterface(interfaceConfig)
+			exists = false
+		} else {
+			if interfaceConfig.OFPort != 0 {
+				klog.V(2).InfoS("Found cached IPSec tunnel interface", "node", nodeName, "interface", interfaceConfig.InterfaceName, "port", interfaceConfig.OFPort)
+				return interfaceConfig.OFPort, nil
+			}
 		}
-	} else {
+	}
+	if !exists {
 		ovsExternalIDs := map[string]interface{}{ovsExternalIDNodeName: nodeName}
 		portUUID, err := c.ovsBridgeClient.CreateTunnelPortExt(
 			portName,
@@ -589,7 +645,6 @@ func (c *Controller) createIPSecTunnelPort(nodeName string, nodeIP net.IP) (int3
 		interfaceConfig.OVSPortConfig = ovsPortConfig
 		c.interfaceStore.AddInterface(interfaceConfig)
 	}
-
 	// GetOFPort will wait for up to 1 second for OVSDB to report the OFPort number.
 	ofPort, err := c.ovsBridgeClient.GetOFPort(interfaceConfig.InterfaceName)
 	if err != nil {
@@ -684,7 +739,7 @@ func getNodeMAC(node *corev1.Node) (net.HardwareAddr, error) {
 }
 
 func (c *Controller) getNodeTransportAddrs(node *corev1.Node) (*utilip.DualStackIPs, error) {
-	var transportAddrs *utilip.DualStackIPs
+	var transportAddrs = new(utilip.DualStackIPs)
 	if c.networkConfig.TransportIface != "" {
 		transportAddrsStr := node.Annotations[types.NodeTransportAddressAnnotationKey]
 		if transportAddrsStr != "" {
diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go
index c8f660b40..5803ba568 100644
--- a/pkg/agent/openflow/client.go
+++ b/pkg/agent/openflow/client.go
@@ -40,7 +40,7 @@ type Client interface {
 	// be called to ensure that the set of OVS flows is correct. All flows programmed in the
 	// switch which match the current round number will be deleted before any new flow is
 	// installed.
-	Initialize(roundInfo types.RoundInfo, config *config.NodeConfig, encapMode config.TrafficEncapModeType) (<-chan struct{}, error)
+	Initialize(roundInfo types.RoundInfo, config *config.NodeConfig, networkconfig *config.NetworkConfig) (<-chan struct{}, error)
 
 	// InstallGatewayFlows sets up flows related to an OVS gateway port, the gateway must exist.
 	InstallGatewayFlows() error
@@ -417,13 +417,13 @@ func (c *client) InstallNodeFlows(hostname string,
 	peerConfigs map[*net.IPNet]net.IP,
 	tunnelPeerIPs *utilip.DualStackIPs,
 	ipsecTunOFPort uint32,
-	remoteGatewayMAC net.HardwareAddr) error {
+	remoteGatewayMAC net.HardwareAddr,
+) error {
 	c.replayMutex.RLock()
 	defer c.replayMutex.RUnlock()
 
 	var flows []binding.Flow
 	localGatewayMAC := c.nodeConfig.GatewayConfig.MAC
-
 	for peerPodCIDR, peerGatewayIP := range peerConfigs {
 		isIPv6 := peerGatewayIP.To4() == nil
 		tunnelPeerIP := tunnelPeerIPs.IPv4
@@ -434,10 +434,10 @@ func (c *client) InstallNodeFlows(hostname string,
 			// only work for IPv4 addresses.
 			flows = append(flows, c.arpResponderFlow(peerGatewayIP, cookie.Node))
 		}
-		if (!isIPv6 && c.encapMode.NeedsEncapToPeer(tunnelPeerIPs.IPv4, c.nodeConfig.NodeTransportIPv4Addr)) ||
-			(isIPv6 && c.encapMode.NeedsEncapToPeer(tunnelPeerIPs.IPv6, c.nodeConfig.NodeTransportIPv6Addr)) {
-			// tunnelPeerIP is the Node Internal Address. In a dual-stack setup, one Node has 2 Node Internal
-			// Addresses (IPv4 and IPv6) .
+		// tunnelPeerIP is the Node Internal Address. In a dual-stack setup, one Node has 2 Node Internal
+		// Addresses (IPv4 and IPv6) .
+		if (!isIPv6 && c.networkConfig.NeedsTunnelToPeer(tunnelPeerIPs.IPv4, c.nodeConfig.NodeTransportIPv4Addr)) ||
+			(isIPv6 && c.networkConfig.NeedsTunnelToPeer(tunnelPeerIPs.IPv6, c.nodeConfig.NodeTransportIPv6Addr)) {
 			flows = append(flows, c.l3FwdFlowToRemote(localGatewayMAC, *peerPodCIDR, tunnelPeerIP, cookie.Node))
 		} else {
 			flows = append(flows, c.l3FwdFlowToRemoteViaRouting(localGatewayMAC, remoteGatewayMAC, cookie.Node, tunnelPeerIP, peerPodCIDR)...)
@@ -481,7 +481,7 @@ func (c *client) InstallPodFlows(interfaceName string, podInterfaceIPs []net.IP,
 	// Add L3 Routing flows to rewrite Pod's dst MAC for all validate IPs.
 	flows = append(flows, c.l3FwdFlowToPod(localGatewayMAC, podInterfaceIPs, podInterfaceMAC, cookie.Pod)...)
 
-	if c.encapMode.IsNetworkPolicyOnly() {
+	if c.networkConfig.TrafficEncapMode.IsNetworkPolicyOnly() {
 		// In policy-only mode, traffic to local Pod is routed based on destination IP.
 		flows = append(flows,
 			c.l3FwdFlowRouteToPod(podInterfaceIPs, podInterfaceMAC, cookie.Pod)...,
@@ -713,7 +713,7 @@ func (c *client) initialize() error {
 	if err := c.ofEntryOperations.AddAll(c.rejectBypassNetworkpolicyFlows(cookie.Default)); err != nil {
 		return fmt.Errorf("failed to install flows to skip generated reject responses: %v", err)
 	}
-	if c.encapMode.IsNetworkPolicyOnly() {
+	if c.networkConfig.TrafficEncapMode.IsNetworkPolicyOnly() {
 		if err := c.setupPolicyOnlyFlows(); err != nil {
 			return fmt.Errorf("failed to setup policy only flows: %w", err)
 		}
@@ -729,14 +729,14 @@ func (c *client) initialize() error {
 	return nil
 }
 
-func (c *client) Initialize(roundInfo types.RoundInfo, nodeConfig *config.NodeConfig, encapMode config.TrafficEncapModeType) (<-chan struct{}, error) {
+func (c *client) Initialize(roundInfo types.RoundInfo, nodeConfig *config.NodeConfig, networkConfig *config.NetworkConfig) (<-chan struct{}, error) {
 	c.nodeConfig = nodeConfig
-	c.encapMode = encapMode
+	c.networkConfig = networkConfig
 
-	if config.IsIPv4Enabled(nodeConfig, encapMode) {
+	if config.IsIPv4Enabled(nodeConfig, c.networkConfig.TrafficEncapMode) {
 		c.ipProtocols = append(c.ipProtocols, binding.ProtocolIP)
 	}
-	if config.IsIPv6Enabled(nodeConfig, encapMode) {
+	if config.IsIPv6Enabled(nodeConfig, c.networkConfig.TrafficEncapMode) {
 		c.ipProtocols = append(c.ipProtocols, binding.ProtocolIPv6)
 	}
 
@@ -993,11 +993,11 @@ func (c *client) InitialTLVMap() error {
 }
 
 func (c *client) IsIPv4Enabled() bool {
-	return config.IsIPv4Enabled(c.nodeConfig, c.encapMode)
+	return config.IsIPv4Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode)
 }
 
 func (c *client) IsIPv6Enabled() bool {
-	return config.IsIPv6Enabled(c.nodeConfig, c.encapMode)
+	return config.IsIPv6Enabled(c.nodeConfig, c.networkConfig.TrafficEncapMode)
 }
 
 // setBasePacketOutBuilder sets base IP properties of a packetOutBuilder which can have more packet data added.
diff --git a/pkg/agent/openflow/pipeline.go b/pkg/agent/openflow/pipeline.go
index 187e30f9b..7adf2cc0a 100644
--- a/pkg/agent/openflow/pipeline.go
+++ b/pkg/agent/openflow/pipeline.go
@@ -295,6 +295,7 @@ type client struct {
 	enableAntreaPolicy bool
 	enableDenyTracking bool
 	enableEgress       bool
+	enableWireGuard    bool
 	roundInfo          types.RoundInfo
 	cookieAllocator    cookie.Allocator
 	bridge             binding.Bridge
@@ -320,7 +321,7 @@ type client struct {
 	// replayMutex provides exclusive access to the OFSwitch to the ReplayFlows method.
 	replayMutex   sync.RWMutex
 	nodeConfig    *config.NodeConfig
-	encapMode     config.TrafficEncapModeType
+	networkConfig *config.NetworkConfig
 	gatewayOFPort uint32
 	// ovsDatapathType is the type of the datapath used by the bridge.
 	ovsDatapathType ovsconfig.OVSDatapathType
@@ -876,7 +877,7 @@ func (c *client) traceflowL2ForwardOutputFlows(dataplaneTag uint8, liveTraffic,
 	flows := []binding.Flow{}
 	l2FwdOutTable := c.pipeline[L2ForwardingOutTable]
 	for _, ipProtocol := range c.ipProtocols {
-		if c.encapMode.SupportsEncap() {
+		if c.networkConfig.TrafficEncapMode.SupportsEncap() {
 			// SendToController and Output if output port is tunnel port.
 			fb1 := l2FwdOutTable.BuildFlow(priorityNormal+3).
 				MatchRegFieldWithValue(TargetOFPortField, config.DefaultTunOFPort).
diff --git a/pkg/agent/openflow/pipeline_windows.go b/pkg/agent/openflow/pipeline_windows.go
index 26604f074..5411fa818 100644
--- a/pkg/agent/openflow/pipeline_windows.go
+++ b/pkg/agent/openflow/pipeline_windows.go
@@ -94,7 +94,7 @@ func (c *client) hostBridgeUplinkFlows(localSubnet net.IPNet, category cookie.Ca
 			Cookie(c.cookieAllocator.Request(category).Raw()).
 			Done(),
 	}
-	if c.encapMode.SupportsNoEncap() {
+	if c.networkConfig.TrafficEncapMode.SupportsNoEncap() {
 		// If NoEncap is enabled, the reply packets from remote Pod can be forwarded to local Pod directly.
 		// by explicitly resubmitting them to serviceHairpinTable and marking "macRewriteMark" at same time.
 		flows = append(flows, c.pipeline[ClassifierTable].BuildFlow(priorityHigh).MatchProtocol(binding.ProtocolIP).
@@ -111,7 +111,7 @@ func (c *client) hostBridgeUplinkFlows(localSubnet net.IPNet, category cookie.Ca
 
 func (c *client) l3FwdFlowToRemoteViaRouting(localGatewayMAC net.HardwareAddr, remoteGatewayMAC net.HardwareAddr,
 	category cookie.Category, peerIP net.IP, peerPodCIDR *net.IPNet) []binding.Flow {
-	if c.encapMode.NeedsDirectRoutingToPeer(peerIP, c.nodeConfig.NodeTransportIPv4Addr) && remoteGatewayMAC != nil {
+	if c.networkConfig.NeedsDirectRoutingToPeer(peerIP, c.nodeConfig.NodeTransportIPv4Addr) && remoteGatewayMAC != nil {
 		ipProto := getIPProtocol(peerIP)
 		l3FwdTable := c.pipeline[l3ForwardingTable]
 		// It enhances Windows Noencap mode performance by bypassing host network.
diff --git a/pkg/agent/route/route_linux.go b/pkg/agent/route/route_linux.go
index b1c49e7c6..df4a6640d 100644
--- a/pkg/agent/route/route_linux.go
+++ b/pkg/agent/route/route_linux.go
@@ -605,7 +605,16 @@ func (c *Client) AddRoutes(podCIDR *net.IPNet, nodeName string, nodeIP, nodeGwIP
 		Dst: podCIDR,
 	}
 	var routes []*netlink.Route
-	if c.networkConfig.TrafficEncapMode.NeedsEncapToPeer(nodeIP, nodeTransportIPAddr) {
+	// If WireGuard is enabled, create a route via WireGuard device regardless of the traffic encapsulation modes.
+	if c.networkConfig.TrafficEncryptionMode == config.TrafficEncryptionModeWireGuard {
+		route.LinkIndex = c.nodeConfig.WireGuardConfig.LinkIndex
+		route.Scope = netlink.SCOPE_LINK
+		if podCIDR.IP.To4() != nil {
+			route.Src = c.nodeConfig.GatewayConfig.IPv4
+		} else {
+			route.Src = c.nodeConfig.GatewayConfig.IPv6
+		}
+	} else if c.networkConfig.NeedsTunnelToPeer(nodeIP, nodeTransportIPAddr) {
 		if podCIDR.IP.To4() == nil {
 			// "on-link" is not identified in IPv6 route entries, so split the configuration into 2 entries.
 			routes = []*netlink.Route{
@@ -619,7 +628,7 @@ func (c *Client) AddRoutes(podCIDR *net.IPNet, nodeName string, nodeIP, nodeGwIP
 		}
 		route.LinkIndex = c.nodeConfig.GatewayConfig.LinkIndex
 		route.Gw = nodeGwIP
-	} else if c.networkConfig.TrafficEncapMode.NeedsDirectRoutingToPeer(nodeIP, nodeTransportIPAddr) {
+	} else if c.networkConfig.NeedsDirectRoutingToPeer(nodeIP, nodeTransportIPAddr) {
 		// NoEncap traffic to Node on the same subnet.
 		// Set the peerNodeIP as next hop.
 		route.Gw = nodeIP
diff --git a/pkg/agent/route/route_windows.go b/pkg/agent/route/route_windows.go
index 8e5b694e3..668ec95c6 100644
--- a/pkg/agent/route/route_windows.go
+++ b/pkg/agent/route/route_windows.go
@@ -118,10 +118,10 @@ func (c *Client) AddRoutes(podCIDR *net.IPNet, nodeName string, peerNodeIP, peer
 		DestinationSubnet: podCIDR,
 		RouteMetric:       util.DefaultMetric,
 	}
-	if c.networkConfig.TrafficEncapMode.NeedsEncapToPeer(peerNodeIP, c.nodeConfig.NodeTransportIPv4Addr) {
+	if c.networkConfig.NeedsTunnelToPeer(peerNodeIP, c.nodeConfig.NodeTransportIPv4Addr) {
 		route.LinkIndex = c.nodeConfig.GatewayConfig.LinkIndex
 		route.GatewayAddress = peerGwIP
-	} else if c.networkConfig.TrafficEncapMode.NeedsDirectRoutingToPeer(peerNodeIP, c.nodeConfig.NodeTransportIPv4Addr) {
+	} else if c.networkConfig.NeedsDirectRoutingToPeer(peerNodeIP, c.nodeConfig.NodeTransportIPv4Addr) {
 		// NoEncap traffic to Node on the same subnet.
 		// Set the peerNodeIP as next hop.
 		route.LinkIndex = c.bridgeInfIndex
diff --git a/pkg/agent/types/annotations.go b/pkg/agent/types/annotations.go
index 586a1b51b..fa7c941be 100644
--- a/pkg/agent/types/annotations.go
+++ b/pkg/agent/types/annotations.go
@@ -20,4 +20,7 @@ const (
 
 	// NodeTransportAddressAnnotationKey represents the key of the interface's IP addresses on which the Node transfers Pod traffic in the Annotations of the Node.
 	NodeTransportAddressAnnotationKey string = "node.antrea.io/transport-addresses"
+
+	// NodeWireGuardPublicAnnotationKey represents the key of the Node's WireGuard public key in the Annotations of the Node.
+	NodeWireGuardPublicAnnotationKey string = "node.antrea.io/wireguard-public-key"
 )
diff --git a/pkg/agent/util/net.go b/pkg/agent/util/net.go
index abaee70f6..dec5cf412 100644
--- a/pkg/agent/util/net.go
+++ b/pkg/agent/util/net.go
@@ -165,9 +165,11 @@ func GetIPNetDeviceByName(ifaceName string) (v4IPNet *net.IPNet, v6IPNet *net.IP
 		if ipNet, ok := addr.(*net.IPNet); ok {
 			if ipNet.IP.IsGlobalUnicast() {
 				if ipNet.IP.To4() != nil {
+					if v4IPNet == nil {
+						v4IPNet = ipNet
+					}
+				} else if v6IPNet == nil {
 					v6IPNet = ipNet
-				} else {
-					v4IPNet = ipNet
 				}
 			}
 		}
diff --git a/pkg/agent/wireguard/client_linux.go b/pkg/agent/wireguard/client_linux.go
new file mode 100644
index 000000000..1aea2d869
--- /dev/null
+++ b/pkg/agent/wireguard/client_linux.go
@@ -0,0 +1,227 @@
+//go:build linux
+
+// Copyright 2021 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package wireguard
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"strconv"
+	"sync"
+
+	"github.com/vishvananda/netlink"
+	"golang.org/x/sys/unix"
+	"golang.zx2c4.com/wireguard/wgctrl"
+	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	apitypes "k8s.io/apimachinery/pkg/types"
+	clientset "k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/util/retry"
+	"k8s.io/klog/v2"
+
+	"antrea.io/antrea/pkg/agent/config"
+	"antrea.io/antrea/pkg/agent/types"
+)
+
+const defaultWireGuardInterfaceName = "antrea-wg0"
+
+var zeroKey = wgtypes.Key{}
+
+// wgctrlClient is an interface to mock wgctrl.Client
+type wgctrlClient interface {
+	io.Closer
+	Devices() ([]*wgtypes.Device, error)
+	Device(name string) (*wgtypes.Device, error)
+	ConfigureDevice(name string, config wgtypes.Config) error
+}
+
+var _ Interface = (*client)(nil)
+
+type client struct {
+	wgClient                wgctrlClient
+	nodeName                string
+	k8sClient               clientset.Interface
+	privateKey              wgtypes.Key
+	peerPublicKeyByNodeName *sync.Map
+	wireGuardConfig         *config.WireGuardConfig
+}
+
+func New(clientSet clientset.Interface, nodeConfig *config.NodeConfig, wireGuardConfig *config.WireGuardConfig) (Interface, error) {
+	wgClient, err := wgctrl.New()
+	if err != nil {
+		return nil, err
+	}
+	if wireGuardConfig.Name == "" {
+		wireGuardConfig.Name = defaultWireGuardInterfaceName
+	}
+	c := &client{
+		wgClient:                wgClient,
+		nodeName:                nodeConfig.Name,
+		k8sClient:               clientSet,
+		wireGuardConfig:         wireGuardConfig,
+		peerPublicKeyByNodeName: &sync.Map{},
+	}
+	return c, nil
+}
+
+func (client *client) Init() error {
+	link := &netlink.Wireguard{LinkAttrs: netlink.LinkAttrs{Name: client.wireGuardConfig.Name, MTU: client.wireGuardConfig.MTU}}
+	err := netlink.LinkAdd(link)
+	// ignore existing link as it may have already been created or managed by userspace process.
+	if err != nil && !errors.Is(err, unix.EEXIST) {
+		if errors.Is(err, unix.EOPNOTSUPP) {
+			return fmt.Errorf("WireGuard not supported by the Linux kernel (netlink: %w), make sure the WireGuard kernel module is loaded", err)
+		}
+		return err
+	}
+	if err := netlink.LinkSetUp(link); err != nil {
+		return err
+	}
+	client.wireGuardConfig.LinkIndex = link.Attrs().Index
+	wgDev, err := client.wgClient.Device(client.wireGuardConfig.Name)
+	if err != nil {
+		return err
+	}
+	client.privateKey = wgDev.PrivateKey
+	// WireGuard private key will be persistent across agent restarts. So we only need to
+	// generate a new private key if it is empty (all zero).
+	if client.privateKey == zeroKey {
+		newPkey, err := wgtypes.GeneratePrivateKey()
+		if err != nil {
+			return err
+		}
+		client.privateKey = newPkey
+	}
+	cfg := wgtypes.Config{
+		PrivateKey:   &client.privateKey,
+		ListenPort:   &client.wireGuardConfig.Port,
+		ReplacePeers: false,
+	}
+	patch, _ := json.Marshal(map[string]interface{}{
+		"metadata": map[string]interface{}{
+			"annotations": map[string]string{
+				types.NodeWireGuardPublicAnnotationKey: client.privateKey.PublicKey().String(),
+			},
+		},
+	})
+	if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+		_, err := client.k8sClient.CoreV1().Nodes().Patch(context.TODO(), client.nodeName, apitypes.MergePatchType, patch, metav1.PatchOptions{})
+		return err
+	}); err != nil {
+		return fmt.Errorf("error when patching the Node with the '%s' annotation: %w", types.NodeWireGuardPublicAnnotationKey, err)
+	}
+
+	return client.wgClient.ConfigureDevice(client.wireGuardConfig.Name, cfg)
+}
+
+func (client *client) RemoveStalePeers(currentPeerPublickeys map[string]string) error {
+	wgdev, err := client.wgClient.Device(client.wireGuardConfig.Name)
+	if err != nil {
+		return err
+	}
+	restoredPeerPublicKeys := make(map[wgtypes.Key]struct{})
+	for _, peer := range wgdev.Peers {
+		restoredPeerPublicKeys[peer.PublicKey] = struct{}{}
+	}
+
+	for nodeName, pubKey := range currentPeerPublickeys {
+		pubKey, err := wgtypes.ParseKey(pubKey)
+		if err != nil {
+			klog.ErrorS(err, "Parse WireGuard public key error", "nodeName", nodeName, "publicKey", pubKey)
+			continue
+		}
+		if _, exist := restoredPeerPublicKeys[pubKey]; exist {
+			// Save known Node name and public key mappings for tracking of public key changes when calling UpdatePeer.
+			client.peerPublicKeyByNodeName.Store(nodeName, pubKey)
+			delete(restoredPeerPublicKeys, pubKey)
+		}
+	}
+	for k := range restoredPeerPublicKeys {
+		if err := client.deletePeerByPublicKey(k); err != nil {
+			klog.ErrorS(err, "Delete WireGuard peer error")
+			return err
+		}
+	}
+	return nil
+}
+
+func (client *client) UpdatePeer(nodeName, publicKeyString string, peerNodeIP net.IP, podCIDRs []*net.IPNet) error {
+	pubKey, err := wgtypes.ParseKey(publicKeyString)
+	if err != nil {
+		return err
+	}
+	var allowedIPs []net.IPNet
+
+	if peerNodeIP.To16() == nil {
+		return fmt.Errorf("peer Node IP is not valid: %s", peerNodeIP.String())
+	}
+
+	for _, cidr := range podCIDRs {
+		allowedIPs = append(allowedIPs, *cidr)
+	}
+
+	if key, exist := client.peerPublicKeyByNodeName.Load(nodeName); exist {
+		cachedPeerPubKey := key.(wgtypes.Key)
+		if cachedPeerPubKey != pubKey {
+			klog.InfoS("WireGuard peer public key updated", "nodeName", nodeName, "publicKey", publicKeyString)
+			// delete old peer by public key.
+			if err := client.deletePeerByPublicKey(cachedPeerPubKey); err != nil {
+				return err
+			}
+		}
+	}
+	endpoint := net.JoinHostPort(peerNodeIP.String(), strconv.Itoa(client.wireGuardConfig.Port))
+	endpointUDP, err := net.ResolveUDPAddr("udp", endpoint)
+	if err != nil {
+		return err
+	}
+	client.peerPublicKeyByNodeName.Store(nodeName, pubKey)
+	peerConfig := wgtypes.PeerConfig{
+		PublicKey:         pubKey,
+		Endpoint:          endpointUDP,
+		AllowedIPs:        allowedIPs,
+		ReplaceAllowedIPs: true,
+	}
+	cfg := wgtypes.Config{
+		ReplacePeers: false,
+		Peers:        []wgtypes.PeerConfig{peerConfig},
+	}
+	return client.wgClient.ConfigureDevice(client.wireGuardConfig.Name, cfg)
+}
+
+func (client *client) deletePeerByPublicKey(pubKey wgtypes.Key) error {
+	cfg := wgtypes.Config{Peers: []wgtypes.PeerConfig{
+		{PublicKey: pubKey, Remove: true},
+	}}
+	return client.wgClient.ConfigureDevice(client.wireGuardConfig.Name, cfg)
+}
+
+func (client *client) DeletePeer(nodeName string) error {
+	key, exist := client.peerPublicKeyByNodeName.Load(nodeName)
+	if !exist {
+		return nil
+	}
+	peerPublicKey := key.(wgtypes.Key)
+	if err := client.deletePeerByPublicKey(peerPublicKey); err != nil {
+		return err
+	}
+	client.peerPublicKeyByNodeName.Delete(nodeName)
+	return nil
+}
diff --git a/pkg/agent/wireguard/client_windows.go b/pkg/agent/wireguard/client_windows.go
new file mode 100644
index 000000000..43c3c49a3
--- /dev/null
+++ b/pkg/agent/wireguard/client_windows.go
@@ -0,0 +1,28 @@
+// +build windows
+
+// Copyright 2021 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package wireguard
+
+import (
+	"fmt"
+
+	clientset "k8s.io/client-go/kubernetes"
+
+	"antrea.io/antrea/pkg/agent/config"
+)
+
+func New(clientSet clientset.Interface, nodeConfig *config.NodeConfig, wireGuardConfig *config.WireGuardConfig) (Interface, error) {
+	return nil, fmt.Errorf("WireGuard is not implemented for windows")
+}
diff --git a/pkg/agent/wireguard/interface.go b/pkg/agent/wireguard/interface.go
new file mode 100644
index 000000000..4b11b3370
--- /dev/null
+++ b/pkg/agent/wireguard/interface.go
@@ -0,0 +1,33 @@
+// Copyright 2021 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package wireguard
+
+import (
+	"net"
+)
+
+type Interface interface {
+	// Init initializes the WireGuard client and sets up the WireGuard device.
+	// It will generate a new private key if necessary and update the public key to the Node's annotation.
+	Init() error
+	// UpdatePeer updates WireGuard peer by provided public key and Node IPs.
+	// It will create a new WireGuard peer if the specified Node is not present in WireGuard device.
+	UpdatePeer(nodeName, publicKeyString string, peerNodeIP net.IP, podCIDRs []*net.IPNet) error
+	// RemoveStalePeers reads existing WireGuard peers from the WireGuard device and deletes those which are not in currentPeerPublickeys.
+	// currentPeerPublickeys is a map of Node names to public keys. It is useful to clean up stale WireGuard peers upon antrea starting.
+	RemoveStalePeers(currentPeerPublickeys map[string]string) error
+	// DeletePeer deletes the WireGuard peer by Node name.
+	DeletePeer(nodeName string) error
+}
diff --git a/pkg/apis/ports.go b/pkg/apis/ports.go
index c22a0bd5a..3138209ad 100644
--- a/pkg/apis/ports.go
+++ b/pkg/apis/ports.go
@@ -22,4 +22,6 @@ const (
 	// AntreaAgentClusterMembershipPort is the default port for the antrea-agent cluster.
 	// A gossip-based cluster will be created in the background when the egress feature is turned on.
 	AntreaAgentClusterMembershipPort = 10351
+	// WireGuardListenPort is the default port for WireGuard encrypted traffic.
+	WireGuardListenPort = 51820
 )
diff --git a/plugins/octant/go.mod b/plugins/octant/go.mod
index abe1095db..aa327884e 100644
--- a/plugins/octant/go.mod
+++ b/plugins/octant/go.mod
@@ -68,13 +68,13 @@ require (
 	go.uber.org/atomic v1.7.0 // indirect
 	go.uber.org/multierr v1.6.0 // indirect
 	go.uber.org/zap v1.16.0 // indirect
-	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
-	golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
+	golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e // indirect
+	golang.org/x/net v0.0.0-20210504132125-bbd867fde50d // indirect
 	golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
 	golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
 	golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
-	golang.org/x/text v0.3.4 // indirect
+	golang.org/x/text v0.3.6 // indirect
 	golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
 	golang.org/x/tools v0.1.1 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
diff --git a/plugins/octant/go.sum b/plugins/octant/go.sum
index c028ed816..9f1fdd2d2 100644
--- a/plugins/octant/go.sum
+++ b/plugins/octant/go.sum
@@ -336,6 +336,7 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -426,8 +427,14 @@ github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6t
 github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
 github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
 github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
+github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
+github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
+github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
+github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
+github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
@@ -483,12 +490,21 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
+github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
 github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
 github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
 github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
+github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
+github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
+github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
+github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
+github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
+github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -659,8 +675,10 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU
 github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
 github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
 github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netlink v1.1.1-0.20210510164352-d17758a128bf/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
 github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
 github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
 github.com/vmware-tanzu/octant v0.17.0 h1:2H2AiQU5C1RiHxxYrrOosDHHI9eV51nP+e9PjRP+c48=
 github.com/vmware-tanzu/octant v0.17.0/go.mod h1:lA32xKa6icUclg+DjAX/E/Id1cTqwCXZUem3RGEp/2A=
 github.com/vmware/go-ipfix v0.5.7/go.mod h1:yzbG1rv+yJ8GeMrRm+MDhOV3akygNZUHLhC1pDoD2AY=
@@ -719,8 +737,9 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI=
+golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -797,12 +816,18 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201024042810-be3efd7ff127/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210504132125-bbd867fde50d h1:nTDGCTeAu2LhcsHTRzjyIUbZHCJ4QePArsm27Hka0UM=
+golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -863,6 +888,7 @@ golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -873,15 +899,26 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -893,8 +930,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -967,6 +1005,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
+golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=


Test patch:
diff --git a/ci/kind/test-e2e-kind.sh b/ci/kind/test-e2e-kind.sh
index 0e8e9a429..d590420c9 100755
--- a/ci/kind/test-e2e-kind.sh
+++ b/ci/kind/test-e2e-kind.sh
@@ -112,7 +112,12 @@ if ! $np; then
     manifest_args="$manifest_args --no-np"
 fi
 
-COMMON_IMAGES_LIST=("gcr.io/kubernetes-e2e-test-images/agnhost:2.8" "projects.registry.vmware.com/library/busybox" "projects.registry.vmware.com/antrea/nginx" "projects.registry.vmware.com/antrea/perftool" "projects.registry.vmware.com/antrea/ipfix-collector:v0.5.4")
+COMMON_IMAGES_LIST=("gcr.io/kubernetes-e2e-test-images/agnhost:2.8" \
+                    "projects.registry.vmware.com/library/busybox"  \
+                    "projects.registry.vmware.com/antrea/nginx" \
+                    "projects.registry.vmware.com/antrea/perftool" \
+                    "projects.registry.vmware.com/antrea/ipfix-collector:v0.5.4" \
+                    "projects.registry.vmware.com/antrea/wireguard-go:0.0.20210424")
 for image in "${COMMON_IMAGES_LIST[@]}"; do
     for i in `seq 3`; do
         docker pull $image && break
@@ -147,11 +152,14 @@ function run_test {
 
   if $coverage; then
       $YML_CMD --kind --encap-mode $current_mode $manifest_args | docker exec -i kind-control-plane dd of=/root/antrea-coverage.yml
+      $YML_CMD --kind --encap-mode $current_mode --wireguard-go $manifest_args | docker exec -i kind-control-plane dd of=/root/antrea-wireguard-go-coverage.yml
       $FLOWAGGREGATOR_YML_CMD --coverage | docker exec -i kind-control-plane dd of=/root/flow-aggregator-coverage.yml
   else
       $YML_CMD --kind --encap-mode $current_mode $manifest_args | docker exec -i kind-control-plane dd of=/root/antrea.yml
+      $YML_CMD --kind --encap-mode $current_mode --wireguard-go $manifest_args | docker exec -i kind-control-plane dd of=/root/antrea-wireguard-go.yml
       $FLOWAGGREGATOR_YML_CMD | docker exec -i kind-control-plane dd of=/root/flow-aggregator.yml
   fi
+
   sleep 1
 
   if $coverage; then
diff --git a/cmd/antrea-agent/options_windows_test.go b/cmd/antrea-agent/options_windows_test.go
index 473bd5f6d..af1eaeb4f 100644
--- a/cmd/antrea-agent/options_windows_test.go
+++ b/cmd/antrea-agent/options_windows_test.go
@@ -66,8 +66,13 @@ func TestCheckUnsupportedFeatures(t *testing.T) {
 			false,
 		},
 		{
-			"IPsec tunnel",
-			AgentConfig{EnableIPSecTunnel: true},
+			"IPsec encryption",
+			AgentConfig{TrafficEncryptionMode: config.TrafficEncryptionModeIPSec.String()},
+			false,
+		},
+		{
+			"WireGuard encryption",
+			AgentConfig{TrafficEncryptionMode: config.TrafficEncryptionModeWireGuard.String()},
 			false,
 		},
 		{
diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go
index 3b975e736..7bb95a2df 100644
--- a/pkg/agent/agent_test.go
+++ b/pkg/agent/agent_test.go
@@ -175,84 +175,93 @@ func TestInitNodeLocalConfig(t *testing.T) {
 		ipNet: transportIPNet,
 	}
 	tests := []struct {
-		name                   string
-		trafficEncapMode       config.TrafficEncapModeType
-		transportInterface     *testTransInterface
-		tunnelType             ovsconfig.TunnelType
-		mtu                    int
-		expectedMTU            int
-		expectedNodeAnnotation map[string]string
+		name                      string
+		trafficEncapMode          config.TrafficEncapModeType
+		transportInterface        *testTransInterface
+		tunnelType                ovsconfig.TunnelType
+		mtu                       int
+		expectedMTU               int
+		expectedNodeLocalIfaceMTU int
+		expectedNodeAnnotation    map[string]string
 	}{
 		{
-			name:                   "noencap mode",
-			trafficEncapMode:       config.TrafficEncapModeNoEncap,
-			mtu:                    0,
-			expectedMTU:            1500,
-			expectedNodeAnnotation: map[string]string{types.NodeMACAddressAnnotationKey: macAddr.String()},
+			name:                      "noencap mode",
+			trafficEncapMode:          config.TrafficEncapModeNoEncap,
+			mtu:                       0,
+			expectedNodeLocalIfaceMTU: 1500,
+			expectedMTU:               1500,
+			expectedNodeAnnotation:    map[string]string{types.NodeMACAddressAnnotationKey: macAddr.String()},
 		},
 		{
-			name:                   "hybrid mode",
-			trafficEncapMode:       config.TrafficEncapModeHybrid,
-			mtu:                    0,
-			expectedMTU:            1500,
-			expectedNodeAnnotation: map[string]string{types.NodeMACAddressAnnotationKey: macAddr.String()},
+			name:                      "hybrid mode",
+			trafficEncapMode:          config.TrafficEncapModeHybrid,
+			mtu:                       0,
+			expectedNodeLocalIfaceMTU: 1500,
+			expectedMTU:               1500,
+			expectedNodeAnnotation:    map[string]string{types.NodeMACAddressAnnotationKey: macAddr.String()},
 		},
 		{
-			name:                   "encap mode, geneve tunnel",
-			trafficEncapMode:       config.TrafficEncapModeEncap,
-			tunnelType:             ovsconfig.GeneveTunnel,
-			mtu:                    0,
-			expectedMTU:            1450,
-			expectedNodeAnnotation: nil,
+			name:                      "encap mode, geneve tunnel",
+			trafficEncapMode:          config.TrafficEncapModeEncap,
+			tunnelType:                ovsconfig.GeneveTunnel,
+			mtu:                       0,
+			expectedNodeLocalIfaceMTU: 1500,
+			expectedMTU:               1450,
+			expectedNodeAnnotation:    nil,
 		},
 		{
-			name:                   "encap mode, mtu specified",
-			trafficEncapMode:       config.TrafficEncapModeEncap,
-			tunnelType:             ovsconfig.GeneveTunnel,
-			mtu:                    1400,
-			expectedMTU:            1400,
-			expectedNodeAnnotation: nil,
+			name:                      "encap mode, mtu specified",
+			trafficEncapMode:          config.TrafficEncapModeEncap,
+			tunnelType:                ovsconfig.GeneveTunnel,
+			mtu:                       1400,
+			expectedNodeLocalIfaceMTU: 1500,
+			expectedMTU:               1400,
+			expectedNodeAnnotation:    nil,
 		},
 		{
-			name:               "noencap mode with transportInterface",
-			trafficEncapMode:   config.TrafficEncapModeNoEncap,
-			transportInterface: testTransportIface,
-			mtu:                0,
-			expectedMTU:        1500,
+			name:                      "noencap mode with transportInterface",
+			trafficEncapMode:          config.TrafficEncapModeNoEncap,
+			transportInterface:        testTransportIface,
+			mtu:                       0,
+			expectedNodeLocalIfaceMTU: 1500,
+			expectedMTU:               1500,
 			expectedNodeAnnotation: map[string]string{
 				types.NodeMACAddressAnnotationKey:       transportIfaceMAC.String(),
 				types.NodeTransportAddressAnnotationKey: transportIP.String(),
 			},
 		},
 		{
-			name:               "hybrid mode with transportInterface",
-			trafficEncapMode:   config.TrafficEncapModeHybrid,
-			transportInterface: testTransportIface,
-			mtu:                0,
-			expectedMTU:        1500,
+			name:                      "hybrid mode with transportInterface",
+			trafficEncapMode:          config.TrafficEncapModeHybrid,
+			transportInterface:        testTransportIface,
+			mtu:                       0,
+			expectedNodeLocalIfaceMTU: 1500,
+			expectedMTU:               1500,
 			expectedNodeAnnotation: map[string]string{
 				types.NodeMACAddressAnnotationKey:       transportIfaceMAC.String(),
 				types.NodeTransportAddressAnnotationKey: transportIP.String(),
 			},
 		},
 		{
-			name:               "encap mode with transportInterface, geneve tunnel",
-			trafficEncapMode:   config.TrafficEncapModeEncap,
-			transportInterface: testTransportIface,
-			tunnelType:         ovsconfig.GeneveTunnel,
-			mtu:                0,
-			expectedMTU:        1450,
+			name:                      "encap mode with transportInterface, geneve tunnel",
+			trafficEncapMode:          config.TrafficEncapModeEncap,
+			transportInterface:        testTransportIface,
+			tunnelType:                ovsconfig.GeneveTunnel,
+			mtu:                       0,
+			expectedNodeLocalIfaceMTU: 1500,
+			expectedMTU:               1450,
 			expectedNodeAnnotation: map[string]string{
 				types.NodeTransportAddressAnnotationKey: transportIP.String(),
 			},
 		},
 		{
-			name:               "encap mode with transportInterface, mtu specified",
-			trafficEncapMode:   config.TrafficEncapModeEncap,
-			transportInterface: testTransportIface,
-			tunnelType:         ovsconfig.GeneveTunnel,
-			mtu:                1400,
-			expectedMTU:        1400,
+			name:                      "encap mode with transportInterface, mtu specified",
+			trafficEncapMode:          config.TrafficEncapModeEncap,
+			transportInterface:        testTransportIface,
+			tunnelType:                ovsconfig.GeneveTunnel,
+			mtu:                       1400,
+			expectedNodeLocalIfaceMTU: 1500,
+			expectedMTU:               1400,
 			expectedNodeAnnotation: map[string]string{
 				types.NodeTransportAddressAnnotationKey: transportIP.String(),
 			},
@@ -285,6 +294,7 @@ func TestInitNodeLocalConfig(t *testing.T) {
 				PodIPv4CIDR:           podCIDR,
 				NodeIPv4Addr:          nodeIPNet,
 				NodeTransportIPv4Addr: nodeIPNet,
+				NodeLocalInterfaceMTU: tt.expectedNodeLocalIfaceMTU,
 				NodeMTU:               tt.expectedMTU,
 				UplinkNetConfig:       new(config.AdapterNetConfig),
 			}
diff --git a/pkg/agent/config/node_config_test.go b/pkg/agent/config/node_config_test.go
new file mode 100644
index 000000000..9e28ec963
--- /dev/null
+++ b/pkg/agent/config/node_config_test.go
@@ -0,0 +1,165 @@
+// Copyright 2021 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNetworkConfig_NeedsTunnelToPeer(t *testing.T) {
+	tests := []struct {
+		name    string
+		nc      *NetworkConfig
+		peerIP  net.IP
+		localIP *net.IPNet
+		expBool bool
+	}{
+		{
+			name: "encap-mode",
+			nc: &NetworkConfig{
+				TrafficEncapMode: TrafficEncapModeEncap,
+			},
+			peerIP: net.ParseIP("192.168.0.5"),
+			localIP: &net.IPNet{
+				IP:   net.IPv4(192, 168, 0, 1),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			expBool: true,
+		},
+		{
+			name: "no-encap-mode",
+			nc: &NetworkConfig{
+				TrafficEncapMode: TrafficEncapModeNoEncap,
+			},
+			peerIP: net.ParseIP("192.168.0.5"),
+			localIP: &net.IPNet{
+				IP:   net.IPv4(192, 168, 0, 1),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			expBool: false,
+		},
+		{
+			name: "hybrid-mode-need-encapsulated",
+			nc: &NetworkConfig{
+				TrafficEncapMode: TrafficEncapModeHybrid,
+			},
+			peerIP: net.ParseIP("10.0.0.0"),
+			localIP: &net.IPNet{
+				IP:   net.IPv4(192, 168, 0, 1),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			expBool: true,
+		},
+		{
+			name: "hybrid-mode-no-need-encapsulated",
+			nc: &NetworkConfig{
+				TrafficEncapMode: TrafficEncapModeHybrid,
+			},
+			peerIP: net.ParseIP("192.168.0.5"),
+			localIP: &net.IPNet{
+				IP:   net.IPv4(192, 168, 0, 1),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			expBool: false,
+		},
+		{
+			name: "WireGuard enabled",
+			nc: &NetworkConfig{
+				TrafficEncapMode:      TrafficEncapModeEncap,
+				TrafficEncryptionMode: TrafficEncryptionModeWireGuard,
+			},
+			peerIP: net.ParseIP("10.0.0.0"),
+			localIP: &net.IPNet{
+				IP:   net.IPv4(192, 168, 0, 1),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			expBool: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actualBool := tt.nc.NeedsTunnelToPeer(tt.peerIP, tt.localIP)
+			assert.Equal(t, tt.expBool, actualBool, "NeedsTunnelToPeer did not return correct result")
+		})
+	}
+}
+
+func TestNetworkConfig_NeedsDirectRoutingToPeer(t *testing.T) {
+	tests := []struct {
+		name    string
+		nc      *NetworkConfig
+		peerIP  net.IP
+		localIP *net.IPNet
+		expBool bool
+	}{
+		{
+			name: "encap-mode",
+			nc: &NetworkConfig{
+				TrafficEncapMode: TrafficEncapModeEncap,
+			},
+			peerIP: net.ParseIP("192.168.0.5"),
+			localIP: &net.IPNet{
+				IP:   net.IPv4(192, 168, 0, 1),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			expBool: false,
+		},
+		{
+			name: "no-encap-mode-need-direct-routing",
+			nc: &NetworkConfig{
+				TrafficEncapMode: TrafficEncapModeNoEncap,
+			},
+			peerIP: net.ParseIP("192.168.0.5"),
+			localIP: &net.IPNet{
+				IP:   net.IPv4(192, 168, 0, 1),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			expBool: true,
+		},
+		{
+			name: "no-encap-mode-no-need-direct-routing",
+			nc: &NetworkConfig{
+				TrafficEncapMode: TrafficEncapModeNoEncap,
+			},
+			peerIP: net.ParseIP("192.168.1.5"),
+			localIP: &net.IPNet{
+				IP:   net.IPv4(192, 168, 0, 1),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			expBool: false,
+		},
+		{
+			name: "hybrid-mode-need-direct-routing",
+			nc: &NetworkConfig{
+				TrafficEncapMode: TrafficEncapModeHybrid,
+			},
+			peerIP: net.ParseIP("192.168.0.5"),
+			localIP: &net.IPNet{
+				IP:   net.IPv4(192, 168, 0, 1),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			expBool: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actualBool := tt.nc.NeedsDirectRoutingToPeer(tt.peerIP, tt.localIP)
+			assert.Equal(t, tt.expBool, actualBool, "NeedsDirectRoutingToPeer did not return correct result")
+		})
+	}
+}
diff --git a/pkg/agent/config/traffic_encap_mode_test.go b/pkg/agent/config/traffic_encap_mode_test.go
index 234a5bb85..23cf15380 100644
--- a/pkg/agent/config/traffic_encap_mode_test.go
+++ b/pkg/agent/config/traffic_encap_mode_test.go
@@ -1,7 +1,6 @@
 package config
 
 import (
-	"net"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -76,117 +75,3 @@ func TestTrafficEncapModeTypeSupports(t *testing.T) {
 		})
 	}
 }
-
-func TestTrafficEncapModeTypeNeedsEncapToPeer(t *testing.T) {
-	tests := []struct {
-		name    string
-		mode    TrafficEncapModeType
-		peerIP  net.IP
-		localIP *net.IPNet
-		expBool bool
-	}{
-		{
-			name:   "encap-mode",
-			mode:   0,
-			peerIP: net.ParseIP("192.168.0.5"),
-			localIP: &net.IPNet{
-				IP:   net.IPv4(192, 168, 0, 1),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
-			expBool: true,
-		},
-		{
-			name:   "no-encap-mode",
-			mode:   1,
-			peerIP: net.ParseIP("192.168.0.5"),
-			localIP: &net.IPNet{
-				IP:   net.IPv4(192, 168, 0, 1),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
-			expBool: false,
-		},
-		{
-			name:   "hybrid-mode-need-encapsulated",
-			mode:   2,
-			peerIP: net.ParseIP("10.0.0.0"),
-			localIP: &net.IPNet{
-				IP:   net.IPv4(192, 168, 0, 1),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
-			expBool: true,
-		},
-		{
-			name:   "hybrid-mode-no-need-encapsulated",
-			mode:   2,
-			peerIP: net.ParseIP("192.168.0.5"),
-			localIP: &net.IPNet{
-				IP:   net.IPv4(192, 168, 0, 1),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
-			expBool: false,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			actualBool := tt.mode.NeedsEncapToPeer(tt.peerIP, tt.localIP)
-			assert.Equal(t, tt.expBool, actualBool, "NeedsEncapToPeer did not return correct result")
-		})
-	}
-}
-
-func TestTrafficEncapModeTypeNeedsDirectRoutingToPeer(t *testing.T) {
-	tests := []struct {
-		name    string
-		mode    TrafficEncapModeType
-		peerIP  net.IP
-		localIP *net.IPNet
-		expBool bool
-	}{
-		{
-			name:   "encap-mode",
-			mode:   0,
-			peerIP: net.ParseIP("192.168.0.5"),
-			localIP: &net.IPNet{
-				IP:   net.IPv4(192, 168, 0, 1),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
-			expBool: false,
-		},
-		{
-			name:   "no-encap-mode-need-direct-routing",
-			mode:   1,
-			peerIP: net.ParseIP("192.168.0.5"),
-			localIP: &net.IPNet{
-				IP:   net.IPv4(192, 168, 0, 1),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
-			expBool: true,
-		},
-		{
-			name:   "no-encap-mode-no-need-direct-routing",
-			mode:   1,
-			peerIP: net.ParseIP("192.168.1.5"),
-			localIP: &net.IPNet{
-				IP:   net.IPv4(192, 168, 0, 1),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
-			expBool: false,
-		},
-		{
-			name:   "hybrid-mode-need-direct-routing",
-			mode:   2,
-			peerIP: net.ParseIP("192.168.0.5"),
-			localIP: &net.IPNet{
-				IP:   net.IPv4(192, 168, 0, 1),
-				Mask: net.IPv4Mask(255, 255, 255, 0),
-			},
-			expBool: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			actualBool := tt.mode.NeedsDirectRoutingToPeer(tt.peerIP, tt.localIP)
-			assert.Equal(t, tt.expBool, actualBool, "NeedsDirectRoutingToPeer did not return correct result")
-		})
-	}
-}
diff --git a/pkg/agent/config/traffic_encryption_mode_test.go b/pkg/agent/config/traffic_encryption_mode_test.go
new file mode 100644
index 000000000..3aebda6cf
--- /dev/null
+++ b/pkg/agent/config/traffic_encryption_mode_test.go
@@ -0,0 +1,66 @@
+// Copyright 2021 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetTrafficEncryptionModes(t *testing.T) {
+	modes := GetTrafficEncryptionModes()
+	expModes := []TrafficEncryptionModeType{0, 1, 2}
+	assert.Equal(t, expModes, modes, "TestGetTrafficEncryptionModes received unexpected encryption modes")
+}
+
+func TestGetTrafficEncryptionModeFromStr(t *testing.T) {
+	tests := []struct {
+		name    string
+		input   string
+		expBool bool
+		expMode TrafficEncryptionModeType
+	}{
+		{"None by default", "none", true, TrafficEncryptionModeNone},
+		{"IPsec", "ipsec", true, TrafficEncryptionModeIPSec},
+		{"WireGuard", "wireguard", true, TrafficEncryptionModeWireGuard},
+		{"Capital case", "IPsec", true, TrafficEncryptionModeIPSec},
+		{"Invalid string", "wire guard", false, TrafficEncryptionModeInvalid},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ok, mode := GetTrafficEncryptionModeFromStr(tt.input)
+			assert.Equal(t, tt.expBool, ok, "GetTrafficEncryptionModeFromStr did not return correct boolean")
+			assert.Equal(t, tt.expMode, mode, "GetTrafficEncryptionModeFromStr did not return correct string")
+		})
+	}
+}
+
+func TestTrafficEncryptionModeType_String(t *testing.T) {
+	tests := []struct {
+		name string
+		m    TrafficEncryptionModeType
+		want string
+	}{
+		{"None", TrafficEncryptionModeNone, "None"},
+		{"IPsec", TrafficEncryptionModeIPSec, "IPsec"},
+		{"WireGuard", TrafficEncryptionModeWireGuard, "WireGuard"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.want, tt.m.String(), "TrafficEncryptionModeType.String did not return correct string representation")
+		})
+	}
+}
diff --git a/pkg/agent/controller/networkpolicy/audit_logging_test.go b/pkg/agent/controller/networkpolicy/audit_logging_test.go
index b83215784..5a837375a 100644
--- a/pkg/agent/controller/networkpolicy/audit_logging_test.go
+++ b/pkg/agent/controller/networkpolicy/audit_logging_test.go
@@ -59,7 +59,7 @@ func newTestAntreaPolicyLogger(bufferLength time.Duration) (*AntreaPolicyLogger,
 }
 
 func newLogInfo(disposition string) (*logInfo, string) {
-	expected := fmt.Sprintf("AntreaPolicyIngressRule AntreaNetworkPolicy:default/test %s 0 SRC: 0.0.0.0 DEST: 1.1.1.1 60 TCP", disposition)
+	expected := fmt.Sprintf("AntreaPolicyIngressRule AntreaNetworkPolicy:default/test %s 0 0.0.0.0 1.1.1.1 60 TCP", disposition)
 	return &logInfo{
 		tableName:   "AntreaPolicyIngressRule",
 		npRef:       "AntreaNetworkPolicy:default/test",
diff --git a/pkg/agent/controller/noderoute/node_route_controller_test.go b/pkg/agent/controller/noderoute/node_route_controller_test.go
index 658116e24..98a3e2e1e 100644
--- a/pkg/agent/controller/noderoute/node_route_controller_test.go
+++ b/pkg/agent/controller/noderoute/node_route_controller_test.go
@@ -72,7 +72,7 @@ func newController(t *testing.T, networkConfig *config.NetworkConfig) (*fakeCont
 	c := NewNodeRouteController(clientset, informerFactory, ofClient, ovsClient, routeClient, interfaceStore, networkConfig, &config.NodeConfig{GatewayConfig: &config.GatewayConfig{
 		IPv4: nil,
 		MAC:  gatewayMAC,
-	}})
+	}}, nil)
 	return &fakeController{
 		Controller:      c,
 		clientset:       clientset,
@@ -231,10 +231,10 @@ func TestIPInPodSubnets(t *testing.T) {
 
 func setup(t *testing.T, ifaces []*interfacestore.InterfaceConfig) (*fakeController, func()) {
 	c, closeFn := newController(t, &config.NetworkConfig{
-		TrafficEncapMode:  0,
-		TunnelType:        ovsconfig.TunnelType("vxlan"),
-		EnableIPSecTunnel: true,
-		IPSecPSK:          "changeme",
+		TrafficEncapMode:      0,
+		TunnelType:            ovsconfig.TunnelType("vxlan"),
+		TrafficEncryptionMode: config.TrafficEncryptionModeIPSec,
+		IPSecPSK:              "changeme",
 	})
 	for _, i := range ifaces {
 		c.interfaceStore.AddInterface(i)
@@ -329,11 +329,18 @@ func TestCreateIPSecTunnelPort(t *testing.T) {
 	c.informerFactory.WaitForCacheSync(stopCh)
 
 	node1PortName := util.GenerateNodeTunnelInterfaceName("xyz-k8s-0-1")
+	node2PortName := util.GenerateNodeTunnelInterfaceName("xyz-k8s-0-2")
 	c.ovsClient.EXPECT().CreateTunnelPortExt(
 		node1PortName, ovsconfig.TunnelType("vxlan"), int32(0),
 		false, "", nodeIP1.String(), "changeme",
 		map[string]interface{}{ovsExternalIDNodeName: "xyz-k8s-0-1"}).Times(1)
+	c.ovsClient.EXPECT().CreateTunnelPortExt(
+		node2PortName, ovsconfig.TunnelType("vxlan"), int32(0),
+		false, "", nodeIP2.String(), "changeme",
+		map[string]interface{}{ovsExternalIDNodeName: "xyz-k8s-0-2"}).Times(1)
 	c.ovsClient.EXPECT().GetOFPort(node1PortName).Return(int32(1), nil)
+	c.ovsClient.EXPECT().GetOFPort(node2PortName).Return(int32(2), nil)
+	c.ovsClient.EXPECT().DeletePort("123").Times(1)
 
 	tests := []struct {
 		name       string
@@ -353,7 +360,8 @@ func TestCreateIPSecTunnelPort(t *testing.T) {
 			name:       "hit cache but interface name changed for the same node",
 			nodeName:   "xyz-k8s-0-2",
 			peerNodeIP: nodeIP2,
-			wantErr:    true,
+			wantErr:    false,
+			want:       2,
 		},
 		{
 			name:       "hit cache and return directly",
diff --git a/pkg/agent/openflow/client_test.go b/pkg/agent/openflow/client_test.go
index 387ab70a0..f0d35e1c3 100644
--- a/pkg/agent/openflow/client_test.go
+++ b/pkg/agent/openflow/client_test.go
@@ -49,7 +49,11 @@ var (
 		IPv6: gwIPv6,
 		MAC:  gwMAC,
 	}
-	nodeConfig = &config.NodeConfig{GatewayConfig: gatewayConfig}
+	nodeConfig = &config.NodeConfig{
+		GatewayConfig:   gatewayConfig,
+		WireGuardConfig: &config.WireGuardConfig{},
+	}
+	networkConfig = &config.NetworkConfig{}
 )
 
 func installNodeFlows(ofClient Client, cacheKey string) (int, error) {
@@ -104,6 +108,7 @@ func TestIdempotentFlowInstallation(t *testing.T) {
 			client.cookieAllocator = cookie.NewAllocator(0)
 			client.ofEntryOperations = m
 			client.nodeConfig = nodeConfig
+			client.networkConfig = networkConfig
 
 			m.EXPECT().AddAll(gomock.Any()).Return(nil).Times(1)
 			// Installing the flows should succeed, and all the flows should be added into the cache.
@@ -132,6 +137,7 @@ func TestIdempotentFlowInstallation(t *testing.T) {
 			client.cookieAllocator = cookie.NewAllocator(0)
 			client.ofEntryOperations = m
 			client.nodeConfig = nodeConfig
+			client.networkConfig = networkConfig
 
 			errorCall := m.EXPECT().AddAll(gomock.Any()).Return(errors.New("Bundle error")).Times(1)
 			m.EXPECT().AddAll(gomock.Any()).Return(nil).After(errorCall)
@@ -173,6 +179,7 @@ func TestFlowInstallationFailed(t *testing.T) {
 			client.cookieAllocator = cookie.NewAllocator(0)
 			client.ofEntryOperations = m
 			client.nodeConfig = nodeConfig
+			client.networkConfig = networkConfig
 
 			// We generate an error for AddAll call.
 			m.EXPECT().AddAll(gomock.Any()).Return(errors.New("Bundle error"))
@@ -207,6 +214,7 @@ func TestConcurrentFlowInstallation(t *testing.T) {
 			client.cookieAllocator = cookie.NewAllocator(0)
 			client.ofEntryOperations = m
 			client.nodeConfig = nodeConfig
+			client.networkConfig = networkConfig
 
 			var concurrentCalls atomic.Value // set to true if we observe concurrent calls
 			timeoutCh := make(chan struct{})
diff --git a/pkg/agent/openflow/network_policy_test.go b/pkg/agent/openflow/network_policy_test.go
index 86bbb6916..efdffd066 100644
--- a/pkg/agent/openflow/network_policy_test.go
+++ b/pkg/agent/openflow/network_policy_test.go
@@ -156,6 +156,7 @@ func TestInstallPolicyRuleFlows(t *testing.T) {
 
 	c = prepareClient(ctrl)
 	c.nodeConfig = &config.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: nil}
+	c.networkConfig = &config.NetworkConfig{}
 	c.ipProtocols = []binding.Protocol{binding.ProtocolIP}
 	defaultAction := crdv1alpha1.RuleActionAllow
 	ruleID1 := uint32(101)
@@ -511,6 +512,7 @@ func TestBatchInstallPolicyRuleFlows(t *testing.T) {
 			c.cookieAllocator = cookie.NewAllocator(0)
 			c.ofEntryOperations = mockOperations
 			c.nodeConfig = &config.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: nil}
+			c.networkConfig = &config.NetworkConfig{}
 			c.ipProtocols = []binding.Protocol{binding.ProtocolIP}
 			c.deterministic = true
 
@@ -636,6 +638,7 @@ func TestInstallPolicyRuleFlowsInDualStackCluster(t *testing.T) {
 
 	c = prepareClient(ctrl)
 	c.nodeConfig = &config.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: podIPv6CIDR}
+	c.networkConfig = &config.NetworkConfig{}
 	c.ipProtocols = []binding.Protocol{binding.ProtocolIP, binding.ProtocolIPv6}
 	defaultAction := crdv1alpha1.RuleActionAllow
 	ruleID1 := uint32(101)
diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go
index 203770d83..abf0bfe9f 100644
--- a/pkg/agent/openflow/testing/mock_openflow.go
+++ b/pkg/agent/openflow/testing/mock_openflow.go
@@ -251,7 +251,7 @@ func (mr *MockClientMockRecorder) InitialTLVMap() *gomock.Call {
 }
 
 // Initialize mocks base method
-func (m *MockClient) Initialize(arg0 types.RoundInfo, arg1 *config.NodeConfig, arg2 config.TrafficEncapModeType) (<-chan struct{}, error) {
+func (m *MockClient) Initialize(arg0 types.RoundInfo, arg1 *config.NodeConfig, arg2 *config.NetworkConfig) (<-chan struct{}, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Initialize", arg0, arg1, arg2)
 	ret0, _ := ret[0].(<-chan struct{})
diff --git a/pkg/agent/wireguard/client_test.go b/pkg/agent/wireguard/client_test.go
new file mode 100644
index 000000000..e490318f0
--- /dev/null
+++ b/pkg/agent/wireguard/client_test.go
@@ -0,0 +1,379 @@
+//go:build linux
+
+// Copyright 2021 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package wireguard
+
+import (
+	"net"
+	"sync"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+
+	"antrea.io/antrea/pkg/agent/config"
+)
+
+type fakeWireGuardClient struct {
+	peers map[wgtypes.Key]wgtypes.Peer
+}
+
+func (f *fakeWireGuardClient) Close() error {
+	return nil
+}
+
+func (f *fakeWireGuardClient) Devices() ([]*wgtypes.Device, error) {
+	return nil, nil
+}
+
+func (f *fakeWireGuardClient) Device(name string) (*wgtypes.Device, error) {
+	var res []wgtypes.Peer
+	for _, p := range f.peers {
+		res = append(res, p)
+	}
+	return &wgtypes.Device{
+		Peers: res,
+	}, nil
+}
+
+func (f *fakeWireGuardClient) ConfigureDevice(name string, cfg wgtypes.Config) error {
+	for _, c := range cfg.Peers {
+		if c.Remove {
+			delete(f.peers, c.PublicKey)
+		} else {
+			f.peers[c.PublicKey] = wgtypes.Peer{
+				PublicKey:  c.PublicKey,
+				Endpoint:   c.Endpoint,
+				AllowedIPs: c.AllowedIPs,
+			}
+		}
+	}
+	return nil
+}
+
+func getFakeClient() *client {
+	return &client{
+		wgClient: &fakeWireGuardClient{},
+		nodeName: "fake-node-1",
+		wireGuardConfig: &config.WireGuardConfig{
+			MTU:  1420,
+			Port: 12345,
+		},
+		peerPublicKeyByNodeName: &sync.Map{},
+	}
+}
+
+func Test_RemoveStalePeers(t *testing.T) {
+	pk1, _ := wgtypes.GeneratePrivateKey()
+	pk2, _ := wgtypes.GeneratePrivateKey()
+	pk3, _ := wgtypes.GeneratePrivateKey()
+	tests := []struct {
+		name                           string
+		existingPeers                  map[wgtypes.Key]wgtypes.Peer
+		inputPublicKeys                map[string]string
+		expectedPeers                  map[wgtypes.Key]wgtypes.Peer
+		expectdPeerPublicKeyByNodeName map[string]wgtypes.Key
+	}{
+		{
+			"pass empty/nil slice should remove all existing peers",
+			map[wgtypes.Key]wgtypes.Peer{
+				pk1.PublicKey(): {PublicKey: pk1.PublicKey()},
+				pk2.PublicKey(): {PublicKey: pk2.PublicKey()},
+			},
+			nil,
+			map[wgtypes.Key]wgtypes.Peer{},
+			map[string]wgtypes.Key{},
+		},
+		{
+			"args has no intersection with existing peers",
+			map[wgtypes.Key]wgtypes.Peer{
+				pk1.PublicKey(): {PublicKey: pk1.PublicKey()},
+				pk2.PublicKey(): {PublicKey: pk2.PublicKey()},
+			},
+			map[string]string{
+				"node3": pk3.PublicKey().String(),
+			},
+			map[wgtypes.Key]wgtypes.Peer{},
+			map[string]wgtypes.Key{},
+		},
+		{
+			"should only keep peers passed by args",
+			map[wgtypes.Key]wgtypes.Peer{
+				pk1.PublicKey(): {PublicKey: pk1.PublicKey()},
+				pk2.PublicKey(): {PublicKey: pk2.PublicKey()},
+				pk3.PublicKey(): {PublicKey: pk3.PublicKey()},
+			},
+			map[string]string{
+				"node3": pk3.PublicKey().String(),
+			},
+			map[wgtypes.Key]wgtypes.Peer{
+				pk3.PublicKey(): {PublicKey: pk3.PublicKey()},
+			},
+			map[string]wgtypes.Key{
+				"node3": pk3.PublicKey(),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client := getFakeClient()
+			fc := &fakeWireGuardClient{peers: tt.existingPeers}
+			client.wgClient = fc
+			err := client.RemoveStalePeers(tt.inputPublicKeys)
+			require.NoError(t, err)
+			assert.Equal(t, tt.expectedPeers, fc.peers)
+			for k, v := range tt.expectdPeerPublicKeyByNodeName {
+				pk, ok := client.peerPublicKeyByNodeName.Load(k)
+				assert.True(t, ok)
+				pubKey, ok := pk.(wgtypes.Key)
+				assert.True(t, ok)
+				assert.Equal(t, v, pubKey)
+			}
+		})
+	}
+}
+
+func Test_UpdatePeer(t *testing.T) {
+	pk1, _ := wgtypes.GeneratePrivateKey()
+	pk2, _ := wgtypes.GeneratePrivateKey()
+	ip1, _, _ := net.ParseCIDR("10.20.30.42/32")
+	ip2, _, _ := net.ParseCIDR("10.20.30.43/32")
+	_, podCIDR1, _ := net.ParseCIDR("172.16.1.0/24")
+	_, podCIDR2, _ := net.ParseCIDR("172.16.2.0/24")
+	listenPort := getFakeClient().wireGuardConfig.Port
+	tests := []struct {
+		name                   string
+		existingPeers          map[string]wgtypes.Peer
+		inputPeerNodeName      string
+		inputPeerNodePublicKey string
+		inputPeerNodeIP        net.IP
+		inputPodCIDRs          []*net.IPNet
+		expectedError          bool
+		expectedPeers          map[wgtypes.Key]wgtypes.Peer
+	}{
+		{
+			"call update peer to add new peers",
+			map[string]wgtypes.Peer{},
+			"fake-node-2",
+			pk1.PublicKey().String(),
+			ip1,
+			[]*net.IPNet{podCIDR1},
+			false,
+			map[wgtypes.Key]wgtypes.Peer{
+				pk1.PublicKey(): {
+					PublicKey: pk1.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+		},
+		{
+			"call update peer to update the public key of an existing peer",
+			map[string]wgtypes.Peer{
+				"fake-node-2": {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+			"fake-node-2",
+			pk2.PublicKey().String(),
+			ip1,
+			[]*net.IPNet{podCIDR1},
+			false,
+			map[wgtypes.Key]wgtypes.Peer{
+				pk2.PublicKey(): {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+		},
+		{
+			"call update peer to update Node IP of an existing peer",
+			map[string]wgtypes.Peer{
+				"fake-node-2": {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+			"fake-node-2",
+			pk2.PublicKey().String(),
+			ip2,
+			[]*net.IPNet{podCIDR1},
+			false,
+			map[wgtypes.Key]wgtypes.Peer{
+				pk2.PublicKey(): {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip2,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+		},
+		{
+			"call update peer to update Pod CIDR of an existing peer",
+			map[string]wgtypes.Peer{
+				"fake-node-2": {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+			"fake-node-2",
+			pk2.PublicKey().String(),
+			ip1,
+			[]*net.IPNet{podCIDR2},
+			false,
+			map[wgtypes.Key]wgtypes.Peer{
+				pk2.PublicKey(): {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR2},
+				},
+			},
+		},
+		{
+			"call update peer with invalid public key",
+			map[string]wgtypes.Peer{
+				"fake-node-2": {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+			"fake-node-2",
+			"invalid key",
+			ip1,
+			[]*net.IPNet{podCIDR1},
+			true,
+			map[wgtypes.Key]wgtypes.Peer{
+				pk2.PublicKey(): {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+		},
+		{
+			"call update peer with nil IP",
+			map[string]wgtypes.Peer{
+				"fake-node-2": {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+			"fake-node-2",
+			pk2.PublicKey().String(),
+			nil,
+			[]*net.IPNet{podCIDR1},
+			true,
+			map[wgtypes.Key]wgtypes.Peer{
+				pk2.PublicKey(): {
+					PublicKey: pk2.PublicKey(),
+					Endpoint: &net.UDPAddr{
+						IP:   ip1,
+						Port: listenPort,
+					},
+					AllowedIPs: []net.IPNet{*podCIDR1},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client := getFakeClient()
+			fc := &fakeWireGuardClient{
+				peers: map[wgtypes.Key]wgtypes.Peer{},
+			}
+			for _, ec := range tt.existingPeers {
+				fc.peers[ec.PublicKey] = ec
+			}
+			client.wgClient = fc
+			err := client.UpdatePeer(tt.inputPeerNodeName, tt.inputPeerNodePublicKey, tt.inputPeerNodeIP, tt.inputPodCIDRs)
+			if tt.expectedError {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+			assert.Equal(t, tt.expectedPeers, fc.peers)
+		})
+	}
+}
+
+func Test_DeletePeer(t *testing.T) {
+	client := getFakeClient()
+	fc := &fakeWireGuardClient{
+		peers: map[wgtypes.Key]wgtypes.Peer{},
+	}
+	client.wgClient = fc
+	pk1, _ := wgtypes.GeneratePrivateKey()
+	ip1, _, _ := net.ParseCIDR("10.20.30.42/32")
+	t.Run("delete non-existing peer", func(tt *testing.T) {
+		err := client.UpdatePeer("fake-node-1", pk1.String(), ip1, nil)
+		require.NoError(tt, err)
+		assert.Len(tt, fc.peers, 1)
+		_, ok := client.peerPublicKeyByNodeName.Load("fake-node-1")
+		assert.True(t, ok)
+		err = client.DeletePeer("fake-node-2")
+		require.NoError(tt, err)
+		assert.Len(tt, fc.peers, 1)
+		_, ok = client.peerPublicKeyByNodeName.Load("fake-node-1")
+		assert.True(t, ok)
+	})
+
+	t.Run("delete existing peer", func(tt *testing.T) {
+		err := client.DeletePeer("fake-node-1")
+		require.NoError(tt, err)
+		assert.Len(tt, fc.peers, 0)
+		_, ok := client.peerPublicKeyByNodeName.Load("fake-node-1")
+		assert.False(t, ok)
+	})
+
+}
diff --git a/test/e2e/antreapolicy_test.go b/test/e2e/antreapolicy_test.go
index d225a1612..e1f51e53b 100644
--- a/test/e2e/antreapolicy_test.go
+++ b/test/e2e/antreapolicy_test.go
@@ -2038,8 +2038,8 @@ func testAuditLoggingBasic(t *testing.T, data *TestData) {
 						continue
 					}
 					expectedNumEntries += 1
-					// The audit log should contain log entry `... Drop <ofPriority> SRC: <x/a IP> DEST: <z/* IP> ...`
-					re := regexp.MustCompile(`Drop [0-9]+ SRC: ` + srcIPs[i] + ` DEST: ` + dstIPs[j])
+					// The audit log should contain log entry `... Drop <ofPriority> <x/a IP> <z/* IP> ...`
+					re := regexp.MustCompile(`Drop [0-9]+ ` + srcIPs[i] + ` ` + dstIPs[j])
 					if re.MatchString(stdout) {
 						actualNumEntries += 1
 					} else {
diff --git a/test/e2e/connectivity_test.go b/test/e2e/connectivity_test.go
index d393c4066..3eecd0634 100644
--- a/test/e2e/connectivity_test.go
+++ b/test/e2e/connectivity_test.go
@@ -197,7 +197,7 @@ func createPodsOnDifferentNodes(t *testing.T, data *TestData, tag string) (podIn
 
 	for _, p := range pods.Items {
 		os := clusterInfo.nodesOS[p.Spec.NodeName]
-		piMap[os] = append(piMap[os], podInfo{p.Name, os})
+		piMap[os] = append(piMap[os], podInfo{p.Name, os, p.Spec.NodeName})
 	}
 	var linIdx, winIdx int
 	for linIdx != len(piMap["linux"]) && winIdx != len(piMap["windows"]) {
@@ -242,29 +242,19 @@ func testPodConnectivityDifferentNodes(t *testing.T, data *TestData) {
 	data.testPodConnectivityDifferentNodes(t)
 }
 
-func (data *TestData) redeployAntrea(t *testing.T, enableIPSec bool) {
+func (data *TestData) redeployAntrea(t *testing.T, option deployAntreaOptions) {
 	var err error
 	// export logs before deleting Antrea
-	if enableIPSec {
-		exportLogs(t, data, "beforeRedeployWithIPsec", false)
-	} else {
-		exportLogs(t, data, "beforeRedploy", false)
-	}
+	exportLogs(t, data, fmt.Sprintf("beforeRedeploy%s", option), false)
 	t.Logf("Deleting Antrea Agent DaemonSet")
 	if err = data.deleteAntrea(defaultTimeout); err != nil {
 		t.Fatalf("Error when deleting Antrea DaemonSet: %v", err)
 	}
-
 	t.Logf("Applying Antrea YAML")
-	if enableIPSec {
-		err = data.deployAntreaIPSec()
-	} else {
-		err = data.deployAntrea()
-	}
+	err = data.deployAntrea(option)
 	if err != nil {
 		t.Fatalf("Error when applying Antrea YAML: %v", err)
 	}
-
 	// After redeploying Antrea with / without IPsec, we wait for watchForRestartsDuration and
 	// count the number of container restarts. watchForRestartsDuration should be large enough
 	// to detect issues, e.g. if there is an issue with the antrea-ipsec container.
@@ -302,7 +292,7 @@ func testPodConnectivityAfterAntreaRestart(t *testing.T, data *TestData) {
 
 	data.runPingMesh(t, podInfos[:numPods], agnhostContainerName)
 
-	data.redeployAntrea(t, false)
+	data.redeployAntrea(t, deployAntreaDefault)
 
 	data.runPingMesh(t, podInfos[:numPods], agnhostContainerName)
 }
diff --git a/test/e2e/fixtures.go b/test/e2e/fixtures.go
index cc183ba9e..0dbb13730 100644
--- a/test/e2e/fixtures.go
+++ b/test/e2e/fixtures.go
@@ -136,7 +136,7 @@ func skipIfFeatureDisabled(tb testing.TB, feature featuregate.Feature, checkAgen
 
 func ensureAntreaRunning(data *TestData) error {
 	log.Println("Applying Antrea YAML")
-	if err := data.deployAntrea(); err != nil {
+	if err := data.deployAntrea(deployAntreaDefault); err != nil {
 		return err
 	}
 	log.Println("Waiting for all Antrea DaemonSet Pods")
diff --git a/test/e2e/framework.go b/test/e2e/framework.go
index 1e118e7ac..ca017e849 100644
--- a/test/e2e/framework.go
+++ b/test/e2e/framework.go
@@ -80,6 +80,8 @@ const (
 	agentContainerName         string = "antrea-agent"
 	antreaYML                  string = "antrea.yml"
 	antreaIPSecYML             string = "antrea-ipsec.yml"
+	antreaWireGuardGoYML       string = "antrea-wireguard-go.yml"
+	antreaWireGuardGoCovYML    string = "antrea-wireguard-go-coverage.yml"
 	antreaCovYML               string = "antrea-coverage.yml"
 	antreaIPSecCovYML          string = "antrea-ipsec-coverage.yml"
 	flowAggregatorYML          string = "flow-aggregator.yml"
@@ -161,8 +163,9 @@ var provider providers.ProviderInterface
 
 // podInfo combines OS info with a Pod name. It is useful when choosing commands and options on Pods of different OS (Windows, Linux).
 type podInfo struct {
-	name string
-	os   string
+	name     string
+	os       string
+	nodeName string
 }
 
 // TestData stores the state required for each test case.
@@ -189,6 +192,43 @@ type PodIPs struct {
 	ipStrings []string
 }
 
+type deployAntreaOptions int
+
+const (
+	deployAntreaDefault deployAntreaOptions = iota
+	deployAntreaIPsec
+	deployAntreaWireGuardGo
+	deployAntreaCoverageOffset
+)
+
+func (o deployAntreaOptions) WithCoverage() deployAntreaOptions {
+	return o + deployAntreaCoverageOffset
+}
+
+func (o deployAntreaOptions) DeployYML() string {
+	return deployAntreaOptionsYML[o]
+}
+
+func (o deployAntreaOptions) String() string {
+	return deployAntreaOptionsString[o]
+}
+
+var (
+	deployAntreaOptionsString = [...]string{
+		"AntreaDefault",
+		"AntreaWithIPSec",
+		"AntreaWithWireGuardGo",
+	}
+	deployAntreaOptionsYML = [...]string{
+		antreaYML,
+		antreaIPSecYML,
+		antreaWireGuardGoYML,
+		antreaCovYML,
+		antreaIPSecCovYML,
+		antreaWireGuardGoCovYML,
+	}
+)
+
 func (p PodIPs) String() string {
 	res := ""
 	if p.ipv4 != nil {
@@ -562,20 +602,12 @@ func (data *TestData) deployAntreaCommon(yamlFile string, extraOptions string, w
 	return nil
 }
 
-// deployAntrea deploys Antrea with the standard manifest.
-func (data *TestData) deployAntrea() error {
-	if testOptions.enableCoverage {
-		return data.deployAntreaCommon(antreaCovYML, "", true)
-	}
-	return data.deployAntreaCommon(antreaYML, "", true)
-}
-
-// deployAntreaIPSec deploys Antrea with IPSec tunnel enabled.
-func (data *TestData) deployAntreaIPSec() error {
+// deployAntrea deploys Antrea with deploy options.
+func (data *TestData) deployAntrea(option deployAntreaOptions) error {
 	if testOptions.enableCoverage {
-		return data.deployAntreaCommon(antreaIPSecCovYML, "", true)
+		option = option.WithCoverage()
 	}
-	return data.deployAntreaCommon(antreaIPSecYML, "", true)
+	return data.deployAntreaCommon(option.DeployYML(), "", true)
 }
 
 // deployAntreaFlowExporter deploys Antrea with flow exporter config params enabled.
diff --git a/test/e2e/ipsec_test.go b/test/e2e/ipsec_test.go
index cb2c1b3d4..9083f810b 100644
--- a/test/e2e/ipsec_test.go
+++ b/test/e2e/ipsec_test.go
@@ -76,7 +76,7 @@ func (data *TestData) readSecurityAssociationsStatus(nodeName string) (up int, c
 // them ping each other.
 func testIPSecTunnelConnectivity(t *testing.T, data *TestData) {
 	t.Logf("Redeploy Antrea with IPSec tunnel enabled")
-	data.redeployAntrea(t, true)
+	data.redeployAntrea(t, deployAntreaIPsec)
 
 	data.testPodConnectivityDifferentNodes(t)
 
@@ -92,7 +92,7 @@ func testIPSecTunnelConnectivity(t *testing.T, data *TestData) {
 	}
 
 	// Restore normal Antrea deployment with IPSec disabled.
-	data.redeployAntrea(t, false)
+	data.redeployAntrea(t, deployAntreaDefault)
 }
 
 // testIPSecDeleteStaleTunnelPorts checks that when switching from IPsec mode to
@@ -100,7 +100,7 @@ func testIPSecTunnelConnectivity(t *testing.T, data *TestData) {
 // correctly.
 func testIPSecDeleteStaleTunnelPorts(t *testing.T, data *TestData) {
 	t.Logf("Redeploy Antrea with IPSec tunnel enabled")
-	data.redeployAntrea(t, true)
+	data.redeployAntrea(t, deployAntreaIPsec)
 
 	nodeName0 := nodeName(0)
 	nodeName1 := nodeName(1)
@@ -132,7 +132,7 @@ func testIPSecDeleteStaleTunnelPorts(t *testing.T, data *TestData) {
 	}
 
 	t.Logf("Redeploy Antrea with IPSec tunnel disabled")
-	data.redeployAntrea(t, false)
+	data.redeployAntrea(t, deployAntreaDefault)
 
 	t.Logf("Checking that tunnel port has been deleted")
 	if err := wait.PollImmediate(defaultInterval, defaultTimeout, func() (found bool, err error) {
diff --git a/test/e2e/performance_test.go b/test/e2e/performance_test.go
index 9182a383a..f2f1033cc 100644
--- a/test/e2e/performance_test.go
+++ b/test/e2e/performance_test.go
@@ -320,7 +320,7 @@ func withPerfTestSetup(fn func(data *TestData), b *testing.B) {
 		b.Fatalf("Error when deleting Antrea DaemonSet: %v", err)
 	}
 	b.Logf("Applying Antrea YAML")
-	if err := data.deployAntrea(); err != nil {
+	if err := data.deployAntrea(deployAntreaDefault); err != nil {
 		b.Fatalf("Error when restarting Antrea: %v", err)
 	}
 	b.Logf("Waiting for all Antrea DaemonSet Pods")
diff --git a/test/e2e/traceflow_test.go b/test/e2e/traceflow_test.go
index da0f4032c..a4f3b8653 100644
--- a/test/e2e/traceflow_test.go
+++ b/test/e2e/traceflow_test.go
@@ -2111,13 +2111,13 @@ func runTestTraceflow(t *testing.T, data *TestData, tc testcase) {
 			}
 		} else {
 			dstPod := tc.tf.Spec.Destination.Pod
-			podIPs := waitForPodIPs(t, data, []podInfo{{dstPod, "linux"}})
+			podIPs := waitForPodIPs(t, data, []podInfo{{dstPod, "linux", ""}})
 			dstPodIPs = podIPs[dstPod]
 		}
 		// Give a little time for Nodes to install OVS flows.
 		time.Sleep(time.Second * 2)
 		// Send an ICMP echo packet from the source Pod to the destination.
-		if err := data.runPingCommandFromTestPod(podInfo{srcPod, "linux"}, testNamespace, dstPodIPs, busyboxContainerName, 2, 0); err != nil {
+		if err := data.runPingCommandFromTestPod(podInfo{srcPod, "linux", ""}, testNamespace, dstPodIPs, busyboxContainerName, 2, 0); err != nil {
 			t.Logf("Ping '%s' -> '%v' failed: ERROR (%v)", srcPod, *dstPodIPs, err)
 		}
 	}
diff --git a/test/e2e/wireguard_test.go b/test/e2e/wireguard_test.go
new file mode 100644
index 000000000..205ba4586
--- /dev/null
+++ b/test/e2e/wireguard_test.go
@@ -0,0 +1,125 @@
+// Copyright 2021 Antrea Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package e2e
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"antrea.io/antrea/pkg/agent/config"
+	"antrea.io/antrea/pkg/apis"
+)
+
+// TestWireGuard checks that Pod traffic across two Nodes over the WireGuard tunnel  by creating
+// multiple Pods across distinct Nodes and having them ping each other. It will also verify that
+// the handshake was established when the wg command line is available.
+func TestWireGuard(t *testing.T) {
+	skipIfNumNodesLessThan(t, 2)
+	skipIfHasWindowsNodes(t)
+	providerIsKind := testOptions.providerName == "kind"
+	if !providerIsKind {
+		for _, node := range clusterInfo.nodes {
+			skipIfMissingKernelModule(t, node.name, []string{"wireguard"})
+		}
+	}
+	data, err := setupTest(t)
+	skipIfEncapModeIsNot(t, data, config.TrafficEncapModeEncap)
+
+	if err != nil {
+		t.Fatalf("Error when setting up test: %v", err)
+	}
+	defer teardownTest(t, data)
+
+	if !providerIsKind {
+		ac := []configChange{
+			{"trafficEncryptionMode", "wireguard", false},
+		}
+		if err := data.mutateAntreaConfigMap(nil, ac, false, true); err != nil {
+			t.Fatalf("Failed to enable WireGuard tunnel: %v", err)
+		}
+		defer func() {
+			ac = []configChange{
+				{"trafficEncryptionMode", "none", false},
+			}
+			if err := data.mutateAntreaConfigMap(nil, ac, false, true); err != nil {
+				t.Fatalf("Failed to disable WireGuard tunnel: %v", err)
+			}
+		}()
+	} else {
+		data.redeployAntrea(t, deployAntreaWireGuardGo)
+		defer data.redeployAntrea(t, deployAntreaDefault)
+	}
+
+	t.Run("testWireGuardTunnelConnectivity", func(t *testing.T) { testWireGuardTunnelConnectivity(t, data) })
+}
+
+func (data *TestData) getWireGuardPeerEndpointsWithHandshake(nodeName string) ([]string, error) {
+	var peerEndpoints []string
+	antreaPodName, err := data.getAntreaPodOnNode(nodeName)
+	if err != nil {
+		return peerEndpoints, err
+	}
+	cmd := []string{"wg"}
+	stdout, stderr, err := data.runCommandFromPod(antreaNamespace, antreaPodName, "wireguard", cmd)
+	if err != nil {
+		return peerEndpoints, fmt.Errorf("error when running 'wg' on '%s': %v - stdout: %s - stderr: %s", nodeName, err, stdout, stderr)
+	}
+	peerConfigs := strings.Split(stdout, "\n\n")
+	if len(peerConfigs) < 1 {
+		return peerEndpoints, fmt.Errorf("invalid 'wg' output on '%s': %v - stdout: %s - stderr: %s", nodeName, err, stdout, stderr)
+	}
+
+	for _, p := range peerConfigs[1:] {
+		lines := strings.Split(p, "\n")
+		if len(lines) < 2 {
+			return peerEndpoints, fmt.Errorf("invalid WireGuard peer config output - %s", p)
+		}
+		peerEndpoint := strings.TrimPrefix(strings.TrimSpace(lines[1]), "endpoint: ")
+		for _, l := range lines {
+			if strings.Contains(l, "latest handshake") {
+				peerEndpoints = append(peerEndpoints, peerEndpoint)
+				break
+			}
+		}
+	}
+	return peerEndpoints, nil
+}
+
+func testWireGuardTunnelConnectivity(t *testing.T, data *TestData) {
+	podInfos, deletePods := createPodsOnDifferentNodes(t, data, "differentnodes")
+	defer deletePods()
+	numPods := 2
+	data.runPingMesh(t, podInfos[:numPods], agnhostContainerName)
+	// wg command is only available in WireGuard sidecar container.
+	if testOptions.providerName == "kind" {
+		nodeName0 := podInfos[0].nodeName
+		nodeName1 := podInfos[1].nodeName
+		endpoints, err := data.getWireGuardPeerEndpointsWithHandshake(nodeName0)
+		require.NoError(t, err)
+		t.Logf("Found peer endpoints %v with handshake established for Node '%s'", endpoints, nodeName0)
+		var nodeIP string
+		for _, n := range clusterInfo.nodes {
+			if n.name == nodeName1 {
+				nodeIP = n.ip
+				break
+			}
+		}
+		assert.Contains(t, endpoints, fmt.Sprintf("%s:%d", nodeIP, apis.WireGuardListenPort))
+	}
+}
diff --git a/test/integration/agent/openflow_test.go b/test/integration/agent/openflow_test.go
index 80c0e5b72..dac34e96b 100644
--- a/test/integration/agent/openflow_test.go
+++ b/test/integration/agent/openflow_test.go
@@ -193,7 +193,7 @@ func TestReplayFlowsNetworkPolicyFlows(t *testing.T) {
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge: %v", err))
 
-	_, err = c.Initialize(roundInfo, &config1.NodeConfig{}, config1.TrafficEncapModeEncap)
+	_, err = c.Initialize(roundInfo, &config1.NodeConfig{}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -280,7 +280,7 @@ func testReplayFlows(t *testing.T) {
 }
 
 func testInitialize(t *testing.T, config *testConfig) {
-	if _, err := c.Initialize(roundInfo, config.nodeConfig, config1.TrafficEncapModeEncap); err != nil {
+	if _, err := c.Initialize(roundInfo, config.nodeConfig, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap}); err != nil {
 		t.Errorf("Failed to initialize openflow client: %v", err)
 	}
 	for _, tableFlow := range prepareDefaultFlows(config) {
@@ -378,7 +378,7 @@ func TestNetworkPolicyFlows(t *testing.T) {
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
-	_, err = c.Initialize(roundInfo, &config1.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: podIPv6CIDR}, config1.TrafficEncapModeEncap)
+	_, err = c.Initialize(roundInfo, &config1.NodeConfig{PodIPv4CIDR: podIPv4CIDR, PodIPv6CIDR: podIPv6CIDR}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -530,7 +530,7 @@ func TestProxyServiceFlows(t *testing.T) {
 	err := ofTestUtils.PrepareOVSBridge(br)
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
-	_, err = c.Initialize(roundInfo, &config1.NodeConfig{}, config1.TrafficEncapModeEncap)
+	_, err = c.Initialize(roundInfo, &config1.NodeConfig{}, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {
@@ -1404,7 +1404,7 @@ func TestSNATFlows(t *testing.T) {
 	require.Nil(t, err, fmt.Sprintf("Failed to prepare OVS bridge %s", br))
 
 	config := prepareConfiguration()
-	_, err = c.Initialize(roundInfo, config.nodeConfig, config1.TrafficEncapModeEncap)
+	_, err = c.Initialize(roundInfo, config.nodeConfig, &config1.NetworkConfig{TrafficEncapMode: config1.TrafficEncapModeEncap})
 	require.Nil(t, err, "Failed to initialize OFClient")
 
 	defer func() {