diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..0979a0d
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "gomod" # See documentation for possible values
+ directory: "/src" # Location of package manifests
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
new file mode 100644
index 0000000..d22dac9
--- /dev/null
+++ b/.github/workflows/golangci-lint.yml
@@ -0,0 +1,45 @@
+name: golangci-lint
+on:
+ push:
+ tags:
+ - v*
+ branches:
+ - main
+ pull_request:
+permissions:
+ contents: read
+ # Optional: allow read access to pull request. Use with `only-new-issues` option.
+ # pull-requests: read
+jobs:
+ golangci:
+ name: lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v3
+ with:
+ # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
+ # version: latest
+
+ # Optional: working directory, useful for monorepos
+ working-directory: src/
+
+ # Optional: golangci-lint command line arguments.
+ # args: --issues-exit-code=0
+
+ # Optional: show only new issues if it's a pull request. The default value is `false`.
+ # only-new-issues: true
+
+ # Optional: if set to true then the all caching functionality will be complete disabled,
+ # takes precedence over all other caching options.
+ # skip-cache: true
+
+ # Optional: if set to true then the action don't cache or restore ~/go/pkg.
+ # skip-pkg-cache: true
+
+ # Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
+ # skip-build-cache: true
diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml
new file mode 100644
index 0000000..984ab72
--- /dev/null
+++ b/.github/workflows/goreleaser.yml
@@ -0,0 +1,42 @@
+name: goreleaser
+
+on:
+ push:
+ tags:
+ - v*
+ workflow_dispatch:
+jobs:
+ goreleaser:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.19
+ cache: true
+ cache-dependency-path: src/go.sum
+ - name: Check GoReleaser Config
+ uses: goreleaser/goreleaser-action@v3
+ with:
+ distribution: goreleaser
+ version: latest
+ args: check
+ workdir: ./src
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v3
+ with:
+ # either 'goreleaser' (default) or 'goreleaser-pro'
+ distribution: goreleaser
+ version: latest
+ args: release --rm-dist
+ workdir: ./src
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution
+ # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dc1d65f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# wiretap configs and logs
+*.log
+*.conf
+
+# binaries
+bin/*
+!bin/.gitkeep
+src/dist
+
+# macOS
+.DS_Store
+
+# drawio
+*.bkp
+*.dtmp
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..2ee90cf
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,20 @@
+MIT No Attribution
+
+Copyright 2022 National Technology & Engineering Solutions of Sandia, LLC
+(NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
+Government retains certain rights in this software.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4f05b07
--- /dev/null
+++ b/README.md
@@ -0,0 +1,395 @@
+
+
+#
Wiretap
+
+Wiretap is a transparent, VPN-like proxy server that tunnels traffic via WireGuard and requires no special privileges to run.
+
+
+In this diagram, the client has generated and installed a WireGuard configuration file that will route traffic destined for `10.0.0.0/24` through a WireGuard interface. Wiretap is then deployed to the server with a configuration that connects to the client as a WireGuard peer. The client can then interact with resources local to the server as if on the same network.
+
+
+
+![Wiretap Diagram](media/Wiretap_Animated.drawio.gif)
+
+
+## Quick Start
+
+1. Download binaries from the [releases](https://github.com/sandialabs/wiretap/releases) page, one for your client machine and one for your server (if different os/arch)
+2. Run `./wiretap configure --port --endpoint --routes ` with the appropriate arguments
+3. Import the resulting `wiretap.conf` file into WireGuard on the client machine
+4. Copy and paste the arguments output from the configure command into Wiretap on the server machine
+
+## Requirements
+
+### Client System
+
+* WireGuard - https://www.wireguard.com/install/
+* Privileged access to configure WireGuard
+
+### Server System
+
+* UDP access to client system's WireGuard endpoint (i.e., UDP traffic can be sent out and come back on at least one port)
+
+While not ideal, Wiretap can still work with outbound TCP instead of UDP. See the [TCP Tunneling](#tcp-tunneling) section for a step-by-step guide.
+
+## Installation
+
+Grab a binary from the [releases](https://github.com/sandialabs/wiretap/releases) page. You may want two binaries if the OS/ARCH are different on the client and server machines.
+
+If you want to compile it yourself or can't find the OS/ARCH you're looking for, install Go (>=1.19) from https://go.dev/dl/ and use the provided [Makefile](./src/Makefile).
+
+## Usage
+
+### Configure
+
+
+
+![Wiretap Configure Arguments](media/Wiretap_Arguments.svg)
+
+
+On the client machine, run Wiretap in configure mode to build a config
+
+```bash
+./wiretap configure --port --endpoint --routes
+```
+
+Following the example in the diagram:
+```bash
+./wiretap configure --port 1337 --endpoint 1.3.3.7:1337 --routes 10.0.0.0/24
+```
+```
+
+Configuration successfully generated.
+Import the config into WireGuard locally and pass the arguments below to Wiretap on the remote machine.
+
+config: wiretap.conf
+────────────────────────────────
+[Interface]
+PrivateKey = qCvx4DBXqemoO8B7eRI2H9Em8zJn++rIBKO+F+ufQWE=
+Address = 192.168.0.2/32
+Address = fd::2/128
+ListenPort = 1337
+
+[Peer]
+PublicKey = 6NxBlwJHujEFr5n9qvFAUyinj0l7Wadd/ZDQMCqTJAA=
+AllowedIPs = 10.0.0.0/24,a::/128
+────────────────────────────────
+
+args: serve --private qGrU0juci5PLJ1ydSufE/UwlErL/bqfcz6uWil705UU= --public ZhRIAcGVwT7l9dhEXv7cvYKwLxOZJR4bgU4zePZaT04= --endpoint 1.3.3.7:1337
+
+```
+
+Install the resulting config either by copying and pasting the output or by importing the new `wiretap.conf` file into WireGuard:
+
+* If using a GUI, select the menu option similar to *Import Tunnel(s) From File*
+* If you have `wg-quick` installed, `sudo wg-quick up ./wiretap.conf`
+
+Don't forget to disable or remove the tunnel when you're done (e.g., `sudo wg-quick down ./wiretap.conf`)
+
+### Deploy
+
+On the remote machine, upload the binary and then copy the command with the private and public keys to start Wiretap in server mode:
+```
+.\wiretap.exe serve --private qGrU0juci5PLJ1ydSufE/UwlErL/bqfcz6uWil705UU= --public ZhRIAcGVwT7l9dhEXv7cvYKwLxOZJR4bgU4zePZaT04= --endpoint 1.3.3.7:1337
+```
+
+Confirm that the client and server have successfully completed the handshake. The client should see a successful handshake in whatever WireGuard interface is running. If using the command-line tools, check with `wg show`.
+
+### Add Peers (optional)
+
+You can create new configurations after deployment for sharing access to the target network with others.
+
+To test access to the Wiretap API running on the server, run:
+
+```bash
+./wiretap ping
+```
+```
+response: pong
+ from: a::
+ time: 2.685600 milliseconds
+```
+
+A successful `pong` message indicates that the API is responsive and commands like `add` will now work.
+
+Adding a peer is very similar to configuring Wiretap initially. It will generate a configuration file you can share, but it will not output arguments that need to be passed to the server because that information is passed via the API. If you're generating a configuration for someone else, get their address information for the endpoint and port flags.
+
+```bash
+./wiretap add --port 1337 --endpoint 1.3.3.8:1337 --routes 10.0.0.0/24
+```
+```
+
+Configuration successfully generated and pushed to server.
+Import this config locally or send it to a friend.
+
+config: wiretap_1.conf
+────────────────────────────────
+[Interface]
+PrivateKey = UJsLCSTg6xqfrKJtXQioaek/mCj4gzOdUIrp/+NkJ3Q=
+Address = 192.168.0.3/32
+Address = fd::3/128
+ListenPort = 1337
+
+[Peer]
+PublicKey = 7mVguCBt7qxMsjDHR7WzzzNXbyBi5Q35gMvyUxjWMWc=
+AllowedIPs = 10.0.0.0/24,a::/128
+────────────────────────────────
+
+```
+
+At this point, the server will attempt to reach out to the provided endpoint. Share the config file and have the recipient import it into WireGuard for Wiretap to connect.
+
+> **Note**
+> To add another peer on the same machine, you will need to specify an unused port, unused routes, and disable the API route.
+
+## Help
+
+```bash
+./wiretap --help --show-hidden
+```
+```
+Usage:
+ wiretap [flags]
+ wiretap [command]
+
+Available Commands:
+ add Add peer to wiretap
+ configure Build wireguard config
+ help Help about any command
+ ping Ping wiretap server API
+ serve Listen and proxy traffic into target network
+
+Flags:
+ -h, --help help for wiretap
+ --show-hidden show hidden flag options
+ -v, --version version for wiretap
+
+Use "wiretap [command] --help" for more information about a command.
+```
+
+## Features
+
+* Network
+ - IPv4
+ - IPv6
+ - ICMPv4: Echo requests and replies
+ - ICMPv6: Echo requests and replies
+* Transport
+ - TCP
+ - Transparent connections
+ - RST response when port is unreachable
+ - UDP
+ - Transparent "connections"
+ - ICMP Destination Unreachable when port is unreachable
+* API
+ - API internal to Wiretap for dynamic configuration
+ - Add peers after deployment for multi-user support
+
+## Demo
+
+The demo has three hosts and two networks:
+
+```
+┌──────────┐
+│ client │
+│ │
+│ 10.1.0.2 │
+│ fd:1::2 ├┬───────────────────────┐
+├──────────┼│ exposed network │
+├──────────┼│ 10.1.0.0/16,fd:1::/64 │
+│ 10.1.0.3 ├┴───────────────────────┘
+│ fd:1::3 │
+│ │
+│ server │
+│ │
+│ 10.2.0.3 │
+│ fd:2::3 ├┬───────────────────────┐
+├──────────┼│ target network │
+├──────────┼│ 10.2.0.0/16,fd:2::/64 │
+│ 10.2.0.4 ├┴───────────────────────┘
+│ fd:2::4 │
+│ │
+│ target │
+└──────────┘
+```
+
+### Video
+
+
+
+
+https://user-images.githubusercontent.com/26662746/202822223-af752660-f263-43dc-bdf1-63140bab316b.mp4
+
+
+### Step-By-Step
+
+You have unprivileged access to the server host and want to reach the target host from the client host using Wiretap.
+
+#### Setup
+
+Clone this repo.
+
+Start the demo containers with:
+```bash
+docker compose up --build
+```
+
+Open new tabs for interactive sessions with the client and server machines:
+```bash
+docker exec -it wiretap-client-1 bash
+```
+```bash
+docker exec -it wiretap-server-1 bash
+```
+
+#### Observe Network Limitations
+
+The target network, and therefore the target host, is unreachable from the client machine. Both the server and target hosts are running a web service on port 80, so try interacting with each of the services from each of the hosts:
+
+Accessing the server's web service from the client should work:
+```bash
+client$ curl http://10.1.0.3
+```
+
+Accessing the target web service from the client should not work, but doing the same thing from the server machine will:
+
+```bash
+# fails
+client$ curl http://10.2.0.4
+```
+```bash
+server$ curl http://10.2.0.4
+```
+
+#### Configure
+
+Configure Wiretap from the client machine. Remember, `--endpoint` is how the server machine should reach the client and `--routes` determines which traffic is routed through Wiretap.
+
+* `--endpoint` needs to be the client address and the default WireGuard port: `10.1.0.2:51820`
+* `--routes` needs to be the subnet of the target network: `10.2.0.0/16`. But there is also an IPv6 subnet, so we should also put `fd:2::/64`. If you just wanted to route traffic to the target host, you could put `10.2.0.4/32` here instead
+
+```bash
+./wiretap_linux_amd64 configure --endpoint 10.1.0.2:51820 --routes 10.2.0.0/16,fd:2::/64
+```
+
+Install the newly created WireGuard config with:
+
+```bash
+wg-quick up ./wiretap.conf
+```
+
+Copy and paste the Wiretap arguments printed by the configure command into the server machine prompt. It should look like this:
+
+```bash
+./wiretap_linux_amd64 serve --private --public --endpoint 10.1.0.2:51820
+```
+
+#### Test
+
+The WireGuard handshake should be complete. Confirm with:
+
+```bash
+wg show
+```
+
+If the handshake was successful the client should be able to reach the target network transparently. Confirm by running the same test that failed before:
+
+```bash
+client$ curl http://10.2.0.4
+```
+
+That's it! Try scanning, pinging, and anything else you can think of (please submit an issue if you think something should work but doesn't!). Here are a few ideas:
+- HTTP
+ - `curl http://10.2.0.4`
+ - `curl http://[fd:2::4]`
+- Nmap
+ - `nmap 10.2.0.4 -v`
+ - `nmap -6 fd:2::4 -v`
+- ICMP
+ - `ping 10.2.0.4`
+ - `ping fd:2::4`
+- UDP
+ - `nmap -sU 10.2.0.4 -v`
+ - `nmap -sU -6 fd:2::4 -v`
+
+#### Teardown
+
+To bring down the WireGuard interface on the client machine, run:
+
+```bash
+wg-quick down ./wiretap.conf
+```
+
+## How It Works
+
+A traditional VPN can't be installed by unprivileged users because VPNs rely on dangerous operations like changing network routes and working with raw packets.
+
+Wiretap bypasses this requirement by rerouting traffic to a user-space TCP/IP network stack, where a listener accepts connections on behalf of the true destination. Then it creates a new connection to the true destination and copies data between the endpoint and the peer. This is similar to how https://github.com/sshuttle/sshuttle works, but relies on WireGuard as the tunneling mechanism rather than SSH.
+
+
+## Experimental
+
+### TCP Tunneling
+
+> **Note**
+> Performance will suffer, only use TCP Tunneling as a last resort
+
+If you have *no* outbound UDP access, you can still use Wiretap, but you'll need to tunnel WireGuard traffic through TCP. This should only be used as a last resort. From WireGuard's [Known Limitations](https://www.wireguard.com/known-limitations/) page:
+> **TCP Mode**
+>
+> WireGuard explicitly does not support tunneling over TCP, due to the classically terrible network performance of tunneling TCP-over-TCP. Rather, transforming WireGuard's UDP packets into TCP is the job of an upper layer of obfuscation (see previous point), and can be accomplished by projects like [udptunnel](https://github.com/rfc1036/udptunnel) and [udp2raw](https://github.com/wangyu-/udp2raw-tunnel).
+
+Another great tool that has similar cross-platform capabilities to Wiretap is [Chisel](https://github.com/jpillora/chisel). We can use chisel to forward a UDP port to the remote system over TCP. To use:
+
+Run chisel server on the client system, specifying a TCP port you can reach from the server system:
+```bash
+./chisel server --port 8080
+```
+
+On the server system, forward the port with this command using the same TCP port you specified in the previous command and using the ListenPort you specified when configuring Wiretap (the default is 51820). The format is `:0.0.0.0:/udp`.
+
+In this example, we're forwarding 51821/udp on the server to 51820 on the client:
+```bash
+./chisel client :8080 51821:0.0.0.0:51820/udp
+```
+
+Finally, run Wiretap with the forwarded local port as your endpoint on the server system:
+```bash
+./wiretap serve --private --public --endpoint localhost:51821
+```
+
+### Nested Tunnels
+
+It is possible to nest multiple WireGuard tunnels using Wiretap, allowing for multiple hops without requiring root on any of the intermediate nodes.
+
+Using this network as an example, we can deploy Wiretap to both hop 1 and hop 2 machines in order to access the target machine on network 3.
+```
+ ┌──────────────────────────────────┐
+┌───────────────┼───────────────┐ ┌───────────────┼────────────────┐
+│ ┌──────────┐ │ ┌──────────┐ │ │ ┌──────────┐ │ ┌──────────┐ │
+│ │ │ │ │ │ │ │ │ │ │ │ │ │
+│ │ client ├──┼─►│ hop 1 ├─┼──┼─►│ hop 2 ├─┼──►│ target │ │
+│ │ │ │ │ │ │ │ │ │ │ │ │ │
+│ └──────────┘ │ └──────────┘ │ │ └──────────┘ │ └──────────┘ │
+└───────────────┼───────────────┘ └───────────────┼────────────────┘
+ network 1: └──────────────────────────────────┘ network 3:
+ 10.0.1.0/24 network 2: 10.0.3.0/24
+ 10.0.2.0/24
+```
+
+After deploying Wiretap to hop 1 normally, re-run the configure command but forgo the endpoint argument because Wiretap currently has no way of tunneling traffic *back* to the client machine if initiated from the server side of the network. In the future Wiretap may support routing between multiple instances of Wiretap.
+
+> **Note**
+> Make sure the routes and port are different from the initial configuration
+
+```bash
+./wiretap configure --port 51821 --routes 10.0.3.0/24
+```
+
+Then deploy Wiretap to hop 2 with the resulting arguments. Because no endpoint was provided, the Endpoint parameter needs to be provided manually to the config file. This depends on the client being able to access hop 2 *through the first hop's instance of Wiretap*! Add the endpoint to the peer section of the new Wiretap config:
+
+```
+Endpoint = 10.0.2.2:51820
+```
+
+Finally, import the config into WireGuard on the client system. The client system will handshake with Wiretap on hop 2 via the tunnel to hop 1, and then all future connections to 10.0.3.0/24 will be routed to network 3 through both hops.
diff --git a/bin/.gitkeep b/bin/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/demo.tape b/demo.tape
new file mode 100644
index 0000000..ad9f2fa
--- /dev/null
+++ b/demo.tape
@@ -0,0 +1,141 @@
+# Made with VHS: https://github.com/charmbracelet/vhs
+# VHS documentation
+#
+# Output:
+# Output .gif Create a GIF output at the given
+# Output .mp4 Create an MP4 output at the given
+# Output .webm Create a WebM output at the given
+#
+# Settings:
+# Set FontSize Set the font size of the terminal
+# Set FontFamily Set the font family of the terminal
+# Set Height Set the height of the terminal
+# Set Width Set the width of the terminal
+# Set LetterSpacing Set the font letter spacing (tracking)
+# Set LineHeight Set the font line height
+# Set Theme Set the theme of the terminal (JSON)
+# Set Padding Set the padding of the terminal
+# Set Framerate Set the framerate of the recording
+# Set PlaybackSpeed Set the playback speed of the recording
+#
+# Sleep:
+# Sleep Sleep for a set amount of in seconds
+#
+# Type:
+# Type[@] "" Type into the terminal with a
+# delay between each character
+#
+# Keys:
+# Backspace[@] [number] Press the Backspace key
+# Down[@] [number] Press the Down key
+# Enter[@] [number] Press the Enter key
+# Space[@] [number] Press the Space key
+# Tab[@] [number] Press the Tab key
+# Left[@] [number] Press the Left Arrow key
+# Right[@] [number] Press the Right Arrow key
+# Up[@] [number] Press the Up Arrow key
+# Down[@] [number] Press the Down Arrow key
+# Ctrl+ Press the Control key + (e.g. Ctrl+C)
+#
+# Display:
+# Hide Hide the subsequent commands from the output
+# Show Show the subsequent commands in the output
+#
+# Run `socat TCP-LISTEN:6000,reuseaddr,fork UNIX-CLIENT:\"$DISPLAY\"` before recording to enable clipboard operations
+
+Output media/wiretap_demo.mp4
+
+Set FontSize 14
+Set Width 1600
+Set Height 800
+Set TypingSpeed 0.1
+Set Padding 20
+# Set Framerate 24
+
+# build and setup
+Hide
+Type "docker compose up --build -d" Enter
+
+Type "tmux" Enter
+
+Type "tmux set -g status off" Enter
+Type "tmux setw -g pane-active-border-style 'fg=green'" Enter
+Type "tmux setw -g pane-border-style 'fg=green'" Enter
+
+Type "docker exec -it wiretap-client-1 bash" Enter
+Type "export PS1='client$ '" Enter
+Ctrl+l
+
+# split window
+Ctrl+b
+Type "%"
+
+Type "docker exec -it wiretap-server-1 bash" Enter
+Type "export PS1='server$ '" Enter
+Ctrl+l
+
+# switch to client
+Ctrl+b
+Left
+Show
+
+# end build and setup
+Set TypingSpeed 0.3
+Sleep 4s
+
+# get machine info
+Type "ip a" Sleep 1s Enter Sleep 2s
+
+Ctrl+b
+Right
+Sleep 2s
+
+Type "ip a" Sleep 1s Enter Sleep 2s
+Type "curl http://target" Sleep 1s Enter Sleep 2s
+Type "nslookup target" Sleep 1s Enter Sleep 2s
+
+Ctrl+b
+Left
+Sleep 2s
+
+# show curl doesn't work, then configure wiretap
+Type "curl http://10.2.0.4 --connect-timeout 3" Sleep 1s Enter Sleep 6s
+Type "./wiretap_linux_arm64 configure --endpoint 10.1.0.2:51820 --routes 10.2.0.0/16,fd:2::/64 -c" Sleep 1s Enter Sleep 4s
+Type "wg-quick up ./wiretap.conf" Sleep 1s Enter Sleep 2s
+
+Ctrl+b
+Left
+Sleep 2s
+
+# args are in clipboard now
+# this is bash magic, ESC+Ctrl+E will expand the current line
+# type ./wiretap_linux_arm64 $(xsel)
+# run expansion to make command line look like it was pasted
+Type "./wiretap_linux_arm64 " Sleep 1s
+Hide
+Type "$(xsel)"
+Escape
+Ctrl+e
+Show
+Sleep 4s
+Enter
+Sleep 3s
+
+Ctrl+b
+Left
+Sleep 2s
+
+Type "wg show" Sleep 1s Enter Sleep 2s
+Type "curl http://10.2.0.4" Sleep 1s Enter Sleep 2s
+
+Sleep 5s
+# shutdown
+Set TypingSpeed 0.1
+Hide
+Ctrl+b
+Type ":kill-session"
+Enter
+
+Type "docker-compose down -t 1" Enter
+Sleep 5s
+# end shutdown
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..55a488a
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,87 @@
+# network layout for wiretap testing
+#
+# ┌──────────┐
+# │ client │
+# │ │
+# │ 10.1.0.2 │
+# │ fd:1::2 ├┬───────────────────────┐
+# ├──────────┼│ exposed network │
+# ├──────────┼│ 10.1.0.0/16,fd:1::/64 │
+# │ 10.1.0.3 ├┴───────────────────────┘
+# │ fd:1::3 │
+# │ │
+# │ server │
+# │ │
+# │ 10.2.0.3 │
+# │ fd:2::3 ├┬───────────────────────┐
+# ├──────────┼│ target network │
+# ├──────────┼│ 10.2.0.0/16,fd:2::/64 │
+# │ 10.2.0.4 ├┴───────────────────────┘
+# │ fd:2::4 │
+# │ │
+# │ target │
+# └──────────┘
+
+services:
+ client:
+ build:
+ context: .
+ dockerfile: wiretap.Dockerfile
+ args:
+ http_proxy: $http_proxy
+ https_proxy: $https_proxy
+ image: wiretap:latest
+ networks:
+ exposed:
+ ipv4_address: 10.1.0.2
+ ipv6_address: fd:1::2
+ cap_add:
+ - NET_ADMIN
+ - SYS_MODULE
+ sysctls:
+ - net.ipv6.conf.all.disable_ipv6=0
+ environment:
+ - DISPLAY=host.docker.internal:0
+ server:
+ depends_on:
+ - client
+ image: wiretap:latest
+ networks:
+ exposed:
+ ipv4_address: 10.1.0.3
+ ipv6_address: fd:1::3
+ target:
+ ipv4_address: 10.2.0.3
+ ipv6_address: fd:2::3
+ environment:
+ - DISPLAY=host.docker.internal:0
+ target:
+ depends_on:
+ - client
+ image: wiretap:latest
+ networks:
+ target:
+ ipv4_address: 10.2.0.4
+ ipv6_address: fd:2::4
+
+networks:
+ exposed:
+ enable_ipv6: true
+ driver: bridge
+ name: exposed
+ ipam:
+ config:
+ - subnet: 10.1.0.0/16
+ gateway: 10.1.0.1
+ - subnet: fd:1::/64
+ gateway: fd:1::1
+ target:
+ enable_ipv6: true
+ driver: bridge
+ name: target
+ ipam:
+ config:
+ - subnet: 10.2.0.0/16
+ gateway: 10.2.0.1
+ - subnet: fd:2::/64
+ gateway: fd:2::1
\ No newline at end of file
diff --git a/media/Wiretap_Animated.drawio b/media/Wiretap_Animated.drawio
new file mode 100644
index 0000000..39e2032
--- /dev/null
+++ b/media/Wiretap_Animated.drawio
@@ -0,0 +1 @@
+7V1tc5u4Fv41nrn3QzQICQEfE8dJu9Om2U2z7d0vHWIrNg0GF3Cc9NdfiTcjJNkQ49TN2p6JHXEQ4pznvOjoCA/QcP50GXuL2cdoQoOBaUyeBuh8YJrQRJh98JbnvMUmTt4wjf1J3mSsG278n7Q4s2xd+hOaFG15UxpFQeovxMZxFIZ0nAptXhxHK5HsPgomQsPCm1JhGLzhZuwFVCL74k/SWd7qWDXqd9SfzsorQ6M4cueNH6ZxtAyL64VRSPMjc6/spiBNZt4kWtWa0GiAhnEUpfm324TGn+6+89szjcC7YyzOaIu7DH3Wox+FZSPKumQdmgZ9pPFzOvPD6cAk7AjjKjorjv395+3ln39++xTemFe3J/HzeIS/niCHM8mb0Jb0ptuN3u7aP+pGDzv2j8xu9KRj91ZHetyRvut4bC39+7Or6/nNP3/cWBenn57Tfz4N8YcTxfDlM1dX6Pvl488RpSfedytdPr6jX070fFWSw45iNjrCqCM96gjrjqiz9KhWckePCiW5onf5TOfj8N1o+W0FffLgw9kfX7/a6QnR40lNr+eTkt7Ww0Ldvx4XSnpLj281fSuAq4eml6GaXi9ENX1XVukhrr71jqKzFBh/Adc6CojoNas1gNueqrfVaoZsvlT3AdgdEWIrELjyfB4ksCjE0I2Fn/XFj2nqLXhcRhdB9EwnA3O4DhtmXjhhQclDW4Oj55ySXG9kLubnSbq6cqJk+Pnmw9Ons5v/nWzwDUpyPcLUvev1RkmvUMv7YPO9JqkXpzK99ma70Ntd+zc6nmAq6AuUmVqUbYRtR5ve1cd09kkd8aXvPrr+2/l5/vzBuX2Pg3eLMPlxO90QCCnJ9V5FSa43GUpyhaJukr2rx4r2XrvQo470+CVYFCZEC+VUSHmxjp7K7hjq24r+W1jvVkPfYNPU9AqQbh3Lxg71zFDTK1zCTgNQhA1Ff1azP37vQioim3PPn4Y0CAbmRf4v+1KbhQsU1clVqiCmYVqf4F9oTtBNJXgvj16wpPVpfpI+lzkJNqlP/bEXfOCpgOso8YvZ/12UptGc3dQsnfMUAWRfS9rTwJ9ymjRacNalcfRQJTQ4nVccH7Ox05g1RMs08EM6rPIqnFsTL5nRSfGPkoKFDQs+zPnTlCeEgLdK0AR4i0XARsHHaXId94NgGAVRnN0PskenZGRUw6odOXWHeHTGh5cs8mvc+098AGcyrwv28xumT7WmgveXNJrTNH5mJMVRyy3yL0VqqsrHrNaJHlJwf1bL8fDZW5Z4KVJL06pns46XQtwdRF/mvP5Nsj8zCbm4UMl+OHTd7Mg+ZG+bDdm7FnB+rfTNNyn9ceBnUpKlKAo8S4/KliF79SRyCwO3/hIBwFMBDfEjA/CEwushwH2LCJh4qTcse5a03BrxtwofDShcXBD2Yu2s7wnHVAM5PSAEQQIcbKxfSICIBS0gg8SGoKSrg8Qx9wUS9BZBQsPJt2WigMgWu7AP34AsByCnBgNLgAEmrmhHsIQJ0wXYlTFBDIDJnmCBWsSNyQNNx7NCDovI54IcPTJGJYWMq4UoSZQ1wLSRz/mFM8Tc2wcNBFbo2QpRNS7bobCJsUdKvbkJVn5Mp0svzqxLFKYX3twPuFSH0TL2WXemcUVXxcFiKRK6/YAqCzCsGmqIaFuIDcqmOpBIaXHqOFq39g+jFi7oCKNfByNpzmJZlamp4wZXaBKAg/cEG+tNOqXDiV2JKfogaMsy32O0qkv6SyInQVrgPsu/jPMD5MeSlzKcifpRNbNv0+IzO/2ubICAkQ5zIZQrFcmgvnqRRuzPDY0ZJ4FwoFrBSDImMR7Venr0Pfb39vw6X/SIFjTM0oLGMutzRvngWVcrj3MhHxRj0V1zoKwtv9WyuYH4rPojAx7H6mrmp/RmkTNlxWAmQr6BnntnTMdjVcR851jYMnrClQMBFqNcqMiCQBcp7YmLQQ8WRbfssM2gsJjxlFf4cP0PvCTxx59nfihyVRTBRmv9fTlflNbalKwNYi3sghd+oHZgTDCjU3KWzVDoZEpvinH6ScYQfzxaN55lOexy7IX1yNqK7jdKl/e+UbY1wZVlSoJBKNpiGnip/yjWOankWFzhmnv6midyxAjGtBtdJIy/Y1qcVc+CNjoym6GQ24iRGWOmNJU6YvzznmtkWSSS6AcMLXHGTwwhO8u+5D2uMVvx9OUwbhFOHSyM26B0K9QPHcama4roc5yXwVjSB8fYD4wJbqgL7hXGurWwPft6U/D1DO9p5rrT2Lu/98eZ+09SFrdN+MUi3i80QPZmN8TjWua32VnTWREGXGYBOrstHhhmo8t8PXPu4YBXi87ny5BnpesBQkxzYfOmIGLhZxlh5CFBHme87YAAQ3F+gS0I5PwGZLNViGWdhKYFekhwaNc0+zWlxgFFBIduJokFRetGGtOKtmbSbsxkkNHO2/dlyaAqsBRM2RpPpdHiB06SDAinjMBdPOU6X9m0Xcyf4noy/fvhRzZZMUbjGTdHf9EfS2YNd7FEzBykol5IU+nmHHzuTyYZVpmZ9H96d1lXVQook551NrDOeV/LNEoKxWkzWy6a4igtC9lRqZzCCgB/9TTxwU08y5l9CBX6ZBp61dnNvqnm00f79kr2zYYNs1Taia72TQKW3S4M7M2+kbdg3xYMr0frtkO6EIHa+pXRgKQjVzpU25b6tnXaXTpvMV0cLSf3cZSljBvR+2jI3yobql8A7wEI0GmYNSSvMUEDlaUvQixPXGDXlzmd3cGg22PUwe8VylTJCG5KlEjMblSaldiwSmdYXLK4yG/v1QhuLBtg+2VezW14Nck99pTcwLYLTDGPgmynAbHd8hu6bX1v0B4tEzYyGl96KV1xjh2IUUKOmMEyHXkxC0IHGIoVB2j0sOCg3QqmCZzu4mYAMvEfm02+IkhRtQmnbo1bJMkgiJGFZVkSbBPHrmEqoPfppjhHlX3qkGbO1raLkZfE9WxV9uoHL801b0QsUKYjBMjYACoqsUgvi1TaLWAazGRi3h5rQ1sKtouImFuEMEtBXtF0FcUPPWFIH6C2j5E3Y2frvFOFnXIeau85MmZ2xWo4GMcEtgwnonDLqIeAWLtvdZsD4g8TWLRnQfVIhGIuMxB3NyjqY4kJmm7eNEC9Zsm1JTY5TOkcuRiuzjXM1NWVqyr7Z6JqVnG04P1b8M0Q3q6LZTQKcB0WhhwFcHAp6igxBhtC4d0wJC/Yfs5C2xaS/qX2s5293FVy5VEkaDTE8qzyVc0nkdeGitVBdJRbC7mVCskMdd2W23JO/HXFCiWxvrpXtCDekEqDLlFsGzogn1juYDr6xF/hE0n7+PSAfSKRi3vLUog3a1s7SO5QfSLW+UTZiB7l9vv4xBbp8n37RGQ4wK7PC8UKYmLYAB30TJHo8zZHr7h/r2hpgPh7eUU5ZVOWL75d69pecofqFeUcEQSIve2j2H5jp9iizD53inIZxh79JGwU01plLVB91QKpGGWCOnv7WHpXP7NIVVN79IO7+sHtENuM4vZqKuILqhbFFPCCEDTLd/uDlCpn063+DBqaNbF6Xf/nJVPF+u68vRWEvZItry+CwdcEkQvsxno8UK3HG1VhhlAiZu8twLJVqavtRn3fNp0Ym0rrbEV9lalQQbIvV2irdmIfTXqPJr2DMlYQbq2MbGKtKrqrtn0DCB0LQwMhjDAprXgNbS4BVr0HxY5wXlrjOi4hFrEQdq0eduxon2e7qyswN5ZH0PSNOgDzFSHXeBIaNEH5mw31+bWqPNgGZG/YOVZjHWg1VnOHoMUfAiB7PazyerYLrH09gsTW71GtBF880CPbLroJGMnCC/XmaVXcETdQYRTPeZFW0z7lz5k11qvQOuyyy+dX2wrp3Qd1/f7qUhiU8Z/y+3/zM7NnmvMHtrGPu+dsc2zHgcuEr6GK24vadlPAPWZsXqCADsBiXEAMVcyO2TzPMmUtxAaAPWQUdM/d1ShhZ/hmG4/Wv6JzMs7ZySn80E/9gqQJ8ux3FgrsGvdxNK8jPj97PF98S+iPTK6mkaYcU9lpqT/nY7MJMHjlvDFPOqP9F/mif50C2LoYmWAITEeaoLXQC/MlhcLs3/UPNeXbEdY/hIVG/wc=
\ No newline at end of file
diff --git a/media/Wiretap_Animated.drawio.gif b/media/Wiretap_Animated.drawio.gif
new file mode 100644
index 0000000..949e7fc
Binary files /dev/null and b/media/Wiretap_Animated.drawio.gif differ
diff --git a/media/Wiretap_Arguments.drawio b/media/Wiretap_Arguments.drawio
new file mode 100644
index 0000000..d1141c7
--- /dev/null
+++ b/media/Wiretap_Arguments.drawio
@@ -0,0 +1 @@
+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
\ No newline at end of file
diff --git a/media/Wiretap_Arguments.svg b/media/Wiretap_Arguments.svg
new file mode 100644
index 0000000..e822415
--- /dev/null
+++ b/media/Wiretap_Arguments.svg
@@ -0,0 +1,3 @@
+
+
+ client$ ./wiretap configure
--port 1337
--endpoint 1.3.3.7:1337
--routes 10.0.0.0/24
client$ ./wiretap configure... --port 1337 configures the Client's WireGuard listening port
--port 1337 config... --endpoint 1.3.3.7:1337 tells the Server how to connect to the Client
--endpoint 1.3.3.7:1337 tells th... --routes 10.0.0.0/24configures the Client's machine to route traffic destined for these subnets through the WireGuard Tunnel
--routes 10.0.0.0/24...
Interna...
Target 10.0.0.3
Server 10.0.0.2
Client 1.3.3.7
WireGuard Tunnel
Internet Text is not SVG - cannot display
\ No newline at end of file
diff --git a/media/wiretap_demo.mp4 b/media/wiretap_demo.mp4
new file mode 100644
index 0000000..e61729b
Binary files /dev/null and b/media/wiretap_demo.mp4 differ
diff --git a/media/wiretap_logo.svg b/media/wiretap_logo.svg
new file mode 100644
index 0000000..061b47b
--- /dev/null
+++ b/media/wiretap_logo.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/.goreleaser.yaml b/src/.goreleaser.yaml
new file mode 100644
index 0000000..fd91bb2
--- /dev/null
+++ b/src/.goreleaser.yaml
@@ -0,0 +1,34 @@
+before:
+ hooks:
+ - go mod tidy
+ - make clean
+builds:
+ - env:
+ - CGO_ENABLED=0
+ ldflags:
+ - -s -w -X wiretap/cmd.Version={{.Version}}
+ flags:
+ - -trimpath
+ goos:
+ - linux
+ - windows
+ - darwin
+ goarch:
+ - arm64
+ - amd64
+ - arm
+ - 386
+checksum:
+ name_template: "checksums.txt"
+snapshot:
+ name_template: "{{ incpatch .Version }}-next"
+changelog:
+ use: github-native
+# sort: asc
+# filters:
+# exclude:
+# - "^docs:"
+# - "^test:"
+# modelines, feel free to remove those if you don't want/use them:
+# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
+# vim: set ts=2 sw=2 tw=0 fo=cnqoj
diff --git a/src/Makefile b/src/Makefile
new file mode 100644
index 0000000..569e6f6
--- /dev/null
+++ b/src/Makefile
@@ -0,0 +1,66 @@
+# wiretap Makefile
+# Double ## are used in the help mesage
+
+VERSION=$(shell git describe --tags --abbrev=0 --always)
+
+# defaults
+OS=$(shell go env GOOS)## Target OS
+ARCH=$(shell go env GOARCH)## Target architecture
+BIN=../bin## Binary location
+
+# env
+GOOS=GOOS=$(OS)
+GOARCH=GOARCH=$(ARCH)
+CGO=CGO_ENABLED=0
+ENV=env $(GOOS) $(GOARCH) $(CGO)
+
+# gobuild
+GOCMD=go
+GOBUILD=$(GOCMD) build
+
+# flags with no arguments
+NOARGFLAGS=-trimpath
+
+# output extension
+ifeq ($(OS), windows)
+ EXT=.exe
+else
+ EXT=
+endif
+
+# ld flags
+LDFLAGS=-s -w
+LDFLAGS+=-X wiretap/cmd.Version=$(VERSION)
+
+.PHONY: all packed wiretap clean help
+.DEFAULT_GOAL := wiretap
+
+## wiretap: Build binary for the specified OS and architecture
+wiretap:
+ $(ENV) $(GOBUILD) $(NOARGFLAGS) -o $(BIN)/$@_$(OS)_$(ARCH)$(EXT) -ldflags "$(LDFLAGS)" *.go
+
+## all: Build binaries for every OS/ARCH pair listed in the Makefile
+all:
+ $(MAKE) OS=windows ARCH=amd64
+ $(MAKE) OS=darwin ARCH=amd64
+ $(MAKE) OS=linux ARCH=amd64
+
+## packed: Build and pack all binaries with upx
+packed: all
+ upx --brute $(BIN)/wiretap_*
+
+
+## clean: Remove all binaries
+clean:
+ rm -vf $(BIN)/*
+
+# reference: https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
+## help: Print this message
+help:
+ @echo "Wiretap Makefile"
+ @echo ""
+ @echo "Targets:"
+ @grep -E '^## [a-zA-Z_-]+: .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ": |## "}; {printf " %-30s %s\n", $$2, $$3}'
+ @echo ""
+ @echo "Variables (KEY=DEFAULT):"
+ @grep -E '^[a-zA-Z_-]+=.+?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = "## "}; {printf " %-30s %s\n", $$1, $$2}'
diff --git a/src/api/api.go b/src/api/api.go
new file mode 100644
index 0000000..fc8b711
--- /dev/null
+++ b/src/api/api.go
@@ -0,0 +1,48 @@
+// Package api handles client-side API requests.
+package api
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "net/http"
+)
+
+// Request packages a URL, method, and request body.
+type Request struct {
+ URL string
+ Method string
+ Body []byte
+}
+
+// MakeRequest attempts to send an API query to the Wiretap server.
+func MakeRequest(req Request) ([]byte, error) {
+ client := &http.Client{}
+ reqBody := bytes.NewBuffer(req.Body)
+
+ r, err := http.NewRequest(req.Method, req.URL, reqBody)
+ if err != nil {
+ return []byte{}, err
+ }
+
+ if len(req.Body) != 0 {
+ r.Header.Add("Content-Type", "application/json")
+ }
+
+ resp, err := client.Do(r)
+ if err != nil {
+ return []byte{}, err
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return []byte{}, err
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return []byte{}, errors.New(string(body))
+ }
+
+ return body, nil
+}
diff --git a/src/cmd/add.go b/src/cmd/add.go
new file mode 100644
index 0000000..0b2a8ce
--- /dev/null
+++ b/src/cmd/add.go
@@ -0,0 +1,191 @@
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/netip"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "github.com/fatih/color"
+ "github.com/spf13/cobra"
+
+ "wiretap/api"
+ "wiretap/peer"
+)
+
+type addCmdConfig struct {
+ allowedIPs []string
+ endpoint string
+ port int
+ configFile string
+ addr4 string
+ addr6 string
+ apiAddr string
+ disableApi bool
+ keepalive int
+}
+
+// Defaults for add command.
+// See root command for shared defaults.
+var addCmd = addCmdConfig{
+ allowedIPs: []string{"0.0.0.0/32"},
+ endpoint: Endpoint,
+ port: Port,
+ configFile: Config,
+ addr4: "",
+ addr6: "",
+ apiAddr: ApiAddr.String(),
+ disableApi: false,
+ keepalive: Keepalive,
+}
+
+// Add command and set flags.
+func init() {
+ // Usage info.
+ cmd := &cobra.Command{
+ Use: "add",
+ Short: "Add peer to wiretap",
+ Long: `Generate configuration for an additional peer and push it to server via API`,
+ Run: func(cmd *cobra.Command, args []string) {
+ addCmd.Run()
+ },
+ }
+
+ rootCmd.AddCommand(cmd)
+
+ cmd.Flags().StringSliceVarP(&addCmd.allowedIPs, "routes", "r", addCmd.allowedIPs, "CIDR IP ranges that will be routed through wiretap")
+ cmd.Flags().StringVarP(&addCmd.endpoint, "endpoint", "e", addCmd.endpoint, "socket address of wireguard listener that server will connect to (example \"1.2.3.4:51820\")")
+ cmd.Flags().IntVarP(&addCmd.port, "port", "p", addCmd.port, "port of local wireguard listener")
+ cmd.Flags().StringVarP(&addCmd.configFile, "output", "o", addCmd.configFile, "wireguard config output filename")
+ cmd.Flags().StringVarP(&addCmd.addr4, "ipv4", "4", addCmd.addr4, "virtual wireguard interface ipv4 address, leave default to let server choose address")
+ cmd.Flags().StringVarP(&addCmd.addr6, "ipv6", "6", addCmd.addr6, "virtual wireguard interface ipv6 address, leave default to let server choose address")
+
+ cmd.Flags().StringVarP(&addCmd.apiAddr, "api", "0", addCmd.apiAddr, "address of server API service")
+ cmd.Flags().BoolVarP(&addCmd.disableApi, "disable-api", "d", addCmd.disableApi, "remove API address from AllowedIPs")
+ cmd.Flags().IntVarP(&addCmd.keepalive, "keepalive", "k", addCmd.keepalive, "tunnel keepalive in seconds")
+
+ cmd.Flags().SortFlags = false
+
+ helpFunc := cmd.HelpFunc()
+ cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
+ if !ShowHidden {
+ for _, f := range []string{"api", "disable-api", "keepalive"} {
+ err := cmd.Flags().MarkHidden(f)
+ if err != nil {
+ fmt.Printf("Failed to hide flag %v: %v\n", f, err)
+ }
+ }
+ }
+ helpFunc(cmd, args)
+ })
+}
+
+// Run attempts to add peer to serve and write new file configuration.
+func (c addCmdConfig) Run() {
+ var err error
+
+ // Disable API
+ if !c.disableApi {
+ c.allowedIPs = append(c.allowedIPs, c.apiAddr)
+ }
+
+ // Query server for public key information. More portable than reading device.
+ apiPrefix, err := netip.ParsePrefix(c.apiAddr)
+ check("failed to parse API address", err)
+ apiAddr := net.JoinHostPort(apiPrefix.Addr().String(), strconv.Itoa(ApiPort))
+
+ req := api.Request{
+ URL: fmt.Sprintf("http://%s/serverinfo", apiAddr),
+ Method: "GET",
+ }
+ body, err := api.MakeRequest(req)
+ check("request failed", err)
+
+ var serverConfig peer.Config
+ err = json.Unmarshal(body, &serverConfig)
+ check("failed to decode response from server", err)
+
+ // Make new configuration for new peer.
+ configArgs := peer.ConfigArgs{
+ ListenPort: c.port,
+ Addresses: []string{c.addr4, c.addr6},
+ Peers: []peer.PeerConfigArgs{
+ {
+ PublicKey: serverConfig.GetPublicKey(),
+ Endpoint: c.endpoint,
+ AllowedIPs: c.allowedIPs,
+ },
+ },
+ }
+
+ config, err := peer.GetConfig(configArgs)
+ check("failed to generate config", err)
+
+ // Server only needs a portion of the information we need to send.
+ newPeerConfig, err := peer.GetPeerConfig(peer.PeerConfigArgs{
+ PublicKey: config.GetPublicKey(),
+ Endpoint: c.endpoint,
+ PersistentKeepaliveInterval: c.keepalive,
+ AllowedIPs: []string{c.addr4, c.addr6},
+ })
+ check("failed to generate peer config", err)
+
+ // Serialize peer config and send to server.
+ body, err = json.Marshal(&newPeerConfig)
+ check("failed to marshal peer config", err)
+ req = api.Request{
+ URL: fmt.Sprintf("http://%s/peers/add", apiAddr),
+ Method: "POST",
+ Body: body,
+ }
+ body, err = api.MakeRequest(req)
+ check("request failed", err)
+
+ err = json.Unmarshal(body, &newPeerConfig)
+ check("failed to parse response", err)
+
+ newAddrs := newPeerConfig.GetAllowedIPs()
+ var newAddrStrings []string
+ for _, addr := range newAddrs {
+ newAddrStrings = append(newAddrStrings, addr.String())
+ }
+ err = config.SetAddresses(newAddrStrings)
+ check("failed to set new addresses", err)
+
+ // Add number to filename if it already exists.
+ count := 1
+ ext := filepath.Ext(c.configFile)
+ basename := strings.TrimSuffix(c.configFile, ext)
+ for {
+ _, err = os.Stat(c.configFile)
+ if os.IsNotExist(err) {
+ break
+ }
+ c.configFile = fmt.Sprintf("%s_%d%s", basename, count, ext)
+ count += 1
+ }
+
+ // Write config file and get status string.
+ var fileStatus string
+ err = os.WriteFile(c.configFile, []byte(config.AsFile()), 0600)
+ if err != nil {
+ fileStatus = Red(fmt.Sprintf("error writing config file: %v", err))
+ } else {
+ fileStatus = Green(c.configFile)
+ }
+
+ // Write and format output.
+ fmt.Fprintln(color.Output)
+ fmt.Fprintln(color.Output, "Configuration successfully generated and pushed to server.")
+ fmt.Fprintln(color.Output, "Import this config locally or send it to a friend.")
+ fmt.Fprintln(color.Output)
+ fmt.Fprintln(color.Output, GreenBold("config:"), fileStatus)
+ fmt.Fprintln(color.Output, Green(strings.Repeat("─", 32)))
+ fmt.Fprint(color.Output, WhiteBold(config.AsFile()))
+ fmt.Fprintln(color.Output, Green(strings.Repeat("─", 32)))
+ fmt.Fprintln(color.Output)
+}
diff --git a/src/cmd/configure.go b/src/cmd/configure.go
new file mode 100644
index 0000000..d94a6e0
--- /dev/null
+++ b/src/cmd/configure.go
@@ -0,0 +1,165 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "wiretap/peer"
+
+ "github.com/atotto/clipboard"
+ "github.com/fatih/color"
+ "github.com/spf13/cobra"
+)
+
+type configureCmdConfig struct {
+ allowedIPs []string
+ endpoint string
+ port int
+ configFile string
+ writeToClipboard bool
+ addr4 string
+ addr6 string
+ apiAddr string
+ disableApi bool
+}
+
+// Defaults for configure command.
+// See root command for shared defaults.
+var configureCmd = configureCmdConfig{
+ allowedIPs: []string{"0.0.0.0/32"},
+ endpoint: Endpoint,
+ port: Port,
+ configFile: Config,
+ writeToClipboard: false,
+ addr4: Subnet4.Addr().Next().Next().String() + "/32",
+ addr6: Subnet6.Addr().Next().Next().String() + "/128",
+ apiAddr: ApiAddr.String(),
+ disableApi: false,
+}
+
+// Add command and set flags.
+func init() {
+ // Usage info.
+ cmd := &cobra.Command{
+ Use: "configure",
+ Short: "Build wireguard config",
+ Long: `Build wireguard config and print command line arguments for deployment`,
+ Run: func(cmd *cobra.Command, args []string) {
+ configureCmd.Run()
+ },
+ }
+
+ rootCmd.AddCommand(cmd)
+
+ cmd.Flags().StringSliceVarP(&configureCmd.allowedIPs, "routes", "r", configureCmd.allowedIPs, "CIDR IP ranges that will be routed through wiretap")
+ cmd.Flags().StringVarP(&configureCmd.endpoint, "endpoint", "e", configureCmd.endpoint, "socket address of wireguard listener that server will connect to (example \"1.2.3.4:51820\")")
+ cmd.Flags().IntVarP(&configureCmd.port, "port", "p", configureCmd.port, "port of local wireguard listener")
+ cmd.Flags().StringVarP(&configureCmd.configFile, "output", "o", configureCmd.configFile, "wireguard config output filename")
+ cmd.Flags().BoolVarP(&configureCmd.writeToClipboard, "clipboard", "c", configureCmd.writeToClipboard, "copy configuration args to clipboard")
+
+ cmd.Flags().StringVarP(&configureCmd.addr4, "ipv4", "4", configureCmd.addr4, "virtual wireguard interface ipv4 address")
+ cmd.Flags().StringVarP(&configureCmd.addr6, "ipv6", "6", configureCmd.addr6, "virtual wireguard interface ipv6 address")
+ cmd.Flags().StringVarP(&configureCmd.apiAddr, "api", "0", configureCmd.apiAddr, "address of server API service")
+ cmd.Flags().BoolVarP(&configureCmd.disableApi, "disable-api", "d", configureCmd.disableApi, "remove API address from AllowedIPs")
+
+ cmd.Flags().SortFlags = false
+
+ helpFunc := cmd.HelpFunc()
+ cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
+ if !ShowHidden {
+ for _, f := range []string{"ipv4", "ipv6", "api", "disable-api"} {
+ err := cmd.Flags().MarkHidden(f)
+ if err != nil {
+ fmt.Printf("Failed to hide flag %v: %v\n", f, err)
+ }
+ }
+ }
+ helpFunc(cmd, args)
+ })
+}
+
+// Run builds a Wireguard config and prints/writes it to a file.
+// Also prints out a command to paste into a remote machine.
+func (c configureCmdConfig) Run() {
+ var err error
+
+ // Set API address if not disabled.
+ if !c.disableApi {
+ c.allowedIPs = append(c.allowedIPs, c.apiAddr)
+ }
+
+ // Use arguments to configure peer.
+ configArgs := peer.ConfigArgs{
+ ListenPort: c.port,
+ Peers: []peer.PeerConfigArgs{
+ {
+ Endpoint: c.endpoint,
+ AllowedIPs: c.allowedIPs,
+ },
+ },
+ Addresses: []string{c.addr4, c.addr6},
+ }
+
+ config, err := peer.GetConfig(configArgs)
+ check("failed to generate config", err)
+
+ // Add number to filename if it already exists.
+ count := 1
+ ext := filepath.Ext(c.configFile)
+ basename := strings.TrimSuffix(c.configFile, ext)
+ for {
+ _, err = os.Stat(c.configFile)
+ if os.IsNotExist(err) {
+ break
+ }
+ c.configFile = fmt.Sprintf("%s_%d%s", basename, count, ext)
+ count += 1
+ }
+
+ // Write config file and get status string.
+ var fileStatus string
+ err = os.WriteFile(c.configFile, []byte(config.AsFile()), 0600)
+ if err != nil {
+ fileStatus = fmt.Sprintf("%s %s", RedBold("config:"), Red(fmt.Sprintf("error writing config file: %v", err)))
+ } else {
+ fileStatus = fmt.Sprintf("%s %s", GreenBold("config:"), Green(c.configFile))
+ }
+
+ // Generate argument string.
+ argString := fmt.Sprintf("serve --private %s --public %s",
+ config.GetPeerPrivateKey(0),
+ config.GetPublicKey(),
+ )
+
+ if len(config.GetPeerEndpoint(0)) > 0 {
+ argString = fmt.Sprintf("%s --endpoint %s", argString, config.GetPeerEndpoint(0))
+ }
+
+ var clipboardStatus string
+ if c.writeToClipboard {
+ err = clipboard.WriteAll(argString)
+ if err != nil {
+ clipboardStatus = fmt.Sprintf("%s %s", RedBold("clipboard:"), Red(fmt.Sprintf("error copying to clipboard: %v", err)))
+ } else {
+ clipboardStatus = fmt.Sprintf("%s %s", GreenBold("clipboard:"), Green("successfully copied"))
+ }
+ }
+
+ // Write and format output.
+ fmt.Fprintln(color.Output)
+ fmt.Fprintln(color.Output, "Configuration successfully generated.")
+ fmt.Fprintln(color.Output, "Import the config into WireGuard locally and pass the arguments below to Wiretap on the remote machine.")
+ fmt.Fprintln(color.Output)
+ fmt.Fprintln(color.Output, fileStatus)
+ fmt.Fprintln(color.Output, Green(strings.Repeat("─", 32)))
+ fmt.Fprint(color.Output, WhiteBold(config.AsFile()))
+ fmt.Fprintln(color.Output, Green(strings.Repeat("─", 32)))
+ fmt.Fprintln(color.Output)
+ fmt.Fprintln(color.Output, GreenBold("args:"), Green(argString))
+ fmt.Fprintln(color.Output)
+ if c.writeToClipboard {
+ fmt.Fprintln(color.Output, clipboardStatus)
+ fmt.Fprintln(color.Output)
+ }
+}
diff --git a/src/cmd/ping.go b/src/cmd/ping.go
new file mode 100644
index 0000000..05c0b4a
--- /dev/null
+++ b/src/cmd/ping.go
@@ -0,0 +1,67 @@
+package cmd
+
+import (
+ "fmt"
+ "net"
+ "net/netip"
+ "strconv"
+ "time"
+
+ "github.com/fatih/color"
+ "github.com/spf13/cobra"
+
+ "wiretap/api"
+)
+
+type pingCmdConfig struct {
+ apiAddr string
+}
+
+// Defaults for ping command.
+// See root command for shared defaults.
+var pingCmd = pingCmdConfig{
+ apiAddr: ApiAddr.String(),
+}
+
+// Add command and set flags.
+func init() {
+ // Usage info.
+ cmd := &cobra.Command{
+ Use: "ping",
+ Short: "Ping wiretap server API",
+ Long: `Test connectivity with wiretap server by querying ping API endpoint`,
+ Run: func(cmd *cobra.Command, args []string) {
+ pingCmd.Run()
+ },
+ }
+
+ rootCmd.AddCommand(cmd)
+
+ cmd.Flags().StringVarP(&pingCmd.apiAddr, "api", "0", pingCmd.apiAddr, "address of server API service")
+
+ cmd.Flags().SortFlags = false
+}
+
+// Run attempts to ping server API and prints response.
+func (c pingCmdConfig) Run() {
+ var err error
+
+ apiPrefix, err := netip.ParsePrefix(c.apiAddr)
+ check("failed to parse API address", err)
+ apiAddr := apiPrefix.Addr()
+
+ req := api.Request{
+ URL: fmt.Sprintf("http://%s/ping", net.JoinHostPort(apiAddr.String(), strconv.Itoa(ApiPort))),
+ Method: "GET",
+ }
+
+ start := time.Now()
+ body, err := api.MakeRequest(req)
+ check("request failed", err)
+
+ duration := time.Since(start)
+
+ fmt.Fprintf(color.Output, "%s: %s\n", GreenBold("response"), Green(string(body)))
+ fmt.Fprintf(color.Output, " %s: %v\n", WhiteBold("from"), apiAddr)
+ fmt.Fprintf(color.Output, " %s: %f %s\n", WhiteBold("time"), float64(duration)/float64(time.Millisecond), Cyan("milliseconds"))
+}
diff --git a/src/cmd/root.go b/src/cmd/root.go
new file mode 100644
index 0000000..4208d33
--- /dev/null
+++ b/src/cmd/root.go
@@ -0,0 +1,71 @@
+// Package cmd handles command line arguments.
+package cmd
+
+import (
+ "fmt"
+ "log"
+ "net/netip"
+ "os"
+
+ "github.com/fatih/color"
+ "github.com/spf13/cobra"
+)
+
+// Defaults shared by multiple commands.
+var (
+ Version = "v0.0.0"
+ Endpoint = ""
+ Port = 51820
+ Config = "wiretap.conf"
+ Keepalive = 25
+ ShowHidden = false
+ ApiAddr = netip.MustParsePrefix("a::/128")
+ ApiPort = 80
+ Subnet4 = netip.MustParsePrefix("192.168.0.0/24")
+ Subnet6 = netip.MustParsePrefix("fd::/64")
+)
+
+// Define colors.
+var (
+ Green = color.New(color.FgGreen).SprintFunc()
+ GreenBold = color.New(color.FgGreen, color.Bold).SprintFunc()
+ Red = color.New(color.FgRed).SprintFunc()
+ RedBold = color.New(color.FgRed, color.Bold).SprintFunc()
+ WhiteBold = color.New(color.FgWhite, color.Bold).SprintFunc()
+ Cyan = color.New(color.FgCyan).SprintFunc()
+)
+
+// Root wiretap command, doesn't do much on its own.
+// Prints help by default.
+var rootCmd = &cobra.Command{
+ Use: "wiretap",
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ err := cmd.Help()
+ if err != nil {
+ fmt.Println("Failed to print help:", err)
+ }
+ os.Exit(0)
+ }
+ },
+ Version: Version,
+ CompletionOptions: cobra.CompletionOptions{
+ HiddenDefaultCmd: true,
+ },
+}
+
+// Execute starts command handling, called by main.
+func Execute() {
+ rootCmd.PersistentFlags().BoolVarP(&ShowHidden, "show-hidden", "", ShowHidden, "show hidden flag options")
+ if err := rootCmd.Execute(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+// check is a helper function that logs and exits if an error is not nil.
+func check(message string, err error) {
+ if err != nil {
+ log.Fatalf("%s: %v", message, err)
+ }
+}
diff --git a/src/cmd/serve.go b/src/cmd/serve.go
new file mode 100644
index 0000000..eb5b2a4
--- /dev/null
+++ b/src/cmd/serve.go
@@ -0,0 +1,237 @@
+package cmd
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net/netip"
+ "os"
+ "strings"
+ "sync"
+
+ "github.com/spf13/cobra"
+ "golang.zx2c4.com/wireguard/conn"
+ "golang.zx2c4.com/wireguard/device"
+ "golang.zx2c4.com/wireguard/tun/netstack"
+
+ "wiretap/peer"
+ "wiretap/transport/api"
+ "wiretap/transport/icmp"
+ "wiretap/transport/tcp"
+ "wiretap/transport/udp"
+)
+
+type serveCmdConfig struct {
+ privateKey string
+ publicKey string
+ endpoint string
+ port int
+ quiet bool
+ debug bool
+ allowedIPs []string
+ addr4 string
+ addr6 string
+ apiAddr string
+ keepalive int
+ mtu int
+ logging bool
+ logFile string
+}
+
+// Defaults for serve command.
+var serveCmd = serveCmdConfig{
+ privateKey: "",
+ publicKey: "",
+ endpoint: Endpoint,
+ port: Port,
+ quiet: false,
+ debug: false,
+ allowedIPs: []string{Subnet4.Addr().Next().Next().String() + "/32", Subnet6.Addr().Next().Next().String() + "/128"},
+ addr4: Subnet4.Addr().Next().String() + "/32",
+ addr6: Subnet6.Addr().Next().String() + "/128",
+ apiAddr: ApiAddr.String(),
+ mtu: 1420,
+ keepalive: Keepalive,
+ logging: false,
+ logFile: "wiretap.log",
+}
+
+// Add serve command and set flags.
+func init() {
+ // Usage info.
+ cmd := &cobra.Command{
+ Use: "serve",
+ Short: "Listen and proxy traffic into target network",
+ Long: `Listen and proxy traffic into target network`,
+ Run: func(cmd *cobra.Command, args []string) {
+ serveCmd.Run()
+ },
+ }
+
+ rootCmd.AddCommand(cmd)
+
+ cmd.Flags().StringVarP(&serveCmd.privateKey, "private", "", serveCmd.privateKey, "wireguard private key")
+ cmd.Flags().StringVarP(&serveCmd.publicKey, "public", "", serveCmd.publicKey, "wireguard public key of remote peer")
+ cmd.Flags().StringVarP(&serveCmd.endpoint, "endpoint", "e", serveCmd.endpoint, "socket address of remote peer that server will connect to (example \"1.2.3.4:51820\")")
+ cmd.Flags().IntVarP(&serveCmd.port, "port", "p", serveCmd.port, "wireguard listener port")
+ cmd.Flags().BoolVarP(&serveCmd.quiet, "quiet", "q", serveCmd.quiet, "silence wiretap log messages")
+ cmd.Flags().BoolVarP(&serveCmd.debug, "debug", "d", serveCmd.debug, "enable wireguard log messages")
+
+ cmd.Flags().StringSliceVarP(&serveCmd.allowedIPs, "allowed", "a", serveCmd.allowedIPs, "comma-separated list of CIDR IP ranges to associate with peer")
+ cmd.Flags().StringVarP(&serveCmd.addr4, "ipv4", "4", serveCmd.addr4, "virtual ipv4 address of wireguard interface")
+ cmd.Flags().StringVarP(&serveCmd.addr6, "ipv6", "6", serveCmd.addr6, "virtual ipv6 address of wireguard interface")
+ cmd.Flags().StringVarP(&serveCmd.apiAddr, "api", "0", serveCmd.apiAddr, "address of API service")
+ cmd.Flags().IntVarP(&serveCmd.keepalive, "keepalive", "k", serveCmd.keepalive, "tunnel keepalive in seconds")
+ cmd.Flags().IntVarP(&serveCmd.mtu, "mtu", "m", serveCmd.mtu, "tunnel MTU")
+ cmd.Flags().BoolVarP(&serveCmd.logging, "log", "l", serveCmd.logging, "enable logging to file")
+ cmd.Flags().StringVarP(&serveCmd.logFile, "log-file", "o", serveCmd.logFile, "write log to this filename")
+
+ // Cannot serve without public key of at least one peer.
+ err := cmd.MarkFlagRequired("public")
+ if err != nil {
+ fmt.Println("Failed to mark public flag as required:", err)
+ }
+
+ // Quiet and debug flags must be used independently.
+ cmd.MarkFlagsMutuallyExclusive("debug", "quiet")
+
+ cmd.Flags().SortFlags = false
+
+ helpFunc := cmd.HelpFunc()
+ cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
+ if !ShowHidden {
+ for _, f := range []string{"allowed", "ipv4", "ipv6", "api", "keepalive", "mtu", "log", "log-file"} {
+ err := cmd.Flags().MarkHidden(f)
+ if err != nil {
+ fmt.Printf("Failed to hide flag %v: %v\n", f, err)
+ }
+ }
+ }
+ helpFunc(cmd, args)
+ })
+}
+
+// Run parses/processes/validates args and then connects to peer,
+// proxying traffic from peer into local network.
+func (c serveCmdConfig) Run() {
+ // Synchronization vars.
+ var (
+ wg sync.WaitGroup
+ lock sync.Mutex
+ )
+
+ // Configure logging.
+ log.SetOutput(os.Stdout)
+ log.SetPrefix("WIRETAP: ")
+ if c.quiet {
+ log.SetOutput(io.Discard)
+ }
+ if c.logging {
+ f, err := os.OpenFile(c.logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+ check("error opening log file", err)
+ defer f.Close()
+
+ if c.quiet {
+ log.SetOutput(f)
+ } else {
+ log.SetOutput(io.MultiWriter(os.Stdout, f))
+ }
+ }
+
+ configArgs := peer.ConfigArgs{
+ PrivateKey: c.privateKey,
+ ListenPort: c.port,
+ Peers: []peer.PeerConfigArgs{
+ {
+ PublicKey: c.publicKey,
+ Endpoint: c.endpoint,
+ PersistentKeepaliveInterval: c.keepalive,
+ AllowedIPs: c.allowedIPs,
+ },
+ },
+ Addresses: []string{c.addr4, c.addr6},
+ }
+
+ config, err := peer.GetConfig(configArgs)
+ check("failed to make configuration", err)
+
+ // Print public key for easier configuration.
+ fmt.Println()
+ fmt.Println("If needed, add this peer to your WireGuard configuration.")
+ fmt.Println()
+ fmt.Println(strings.Repeat("─", 32))
+ fmt.Print(config.AsShareableFile())
+ fmt.Println(strings.Repeat("─", 32))
+ fmt.Println()
+
+ // Create virtual interface with this address and MTU.
+ ipv4Prefix, err := netip.ParsePrefix(c.addr4)
+ check("failed to parse ipv4 address", err)
+ ipv4Addr := ipv4Prefix.Addr()
+
+ ipv6Prefix, err := netip.ParsePrefix(c.addr6)
+ check("failed to parse ipv6 address", err)
+ ipv6Addr := ipv6Prefix.Addr()
+
+ apiPrefix, err := netip.ParsePrefix(c.apiAddr)
+ check("failed to parse API address", err)
+ apiAddr := apiPrefix.Addr()
+
+ tun, tnet, err := netstack.CreateNetTUN(
+ []netip.Addr{ipv4Addr, ipv6Addr, apiAddr},
+ []netip.Addr{},
+ c.mtu,
+ )
+ check("failed to create TUN", err)
+
+ // Make new device.
+ var logger int
+ if c.debug {
+ logger = device.LogLevelVerbose
+ } else if c.quiet {
+ logger = device.LogLevelSilent
+ } else {
+ logger = device.LogLevelError
+ }
+ dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(logger, ""))
+
+ // Configure wireguard.
+ fmt.Println(config.AsIPC())
+ err = dev.IpcSet(config.AsIPC())
+ check("failed to configure wireguard device", err)
+
+ err = dev.Up()
+ check("failed to bring up device", err)
+
+ // Start transport layer handlers.
+ wg.Add(1)
+ lock.Lock()
+ go func() {
+ tcp.Handle(tnet, ipv4Addr, ipv6Addr, 1337, &lock)
+ wg.Done()
+ }()
+
+ lock.Lock()
+ wg.Add(1)
+ go func() {
+ udp.Handle(tnet, ipv4Addr, ipv6Addr, 1337, &lock)
+ wg.Done()
+ }()
+
+ lock.Lock()
+ wg.Add(1)
+ go func() {
+ icmp.Handle(tnet, &lock)
+ wg.Done()
+ }()
+
+ // Start API handler. Starting last because firewall rule needs to be first.
+ lock.Lock()
+ wg.Add(1)
+ go func() {
+ api.Handle(tnet, dev, &config, apiAddr, uint16(ApiPort), &lock)
+ wg.Done()
+ }()
+
+ wg.Wait()
+}
diff --git a/src/go.mod b/src/go.mod
new file mode 100644
index 0000000..55e8d43
--- /dev/null
+++ b/src/go.mod
@@ -0,0 +1,34 @@
+module wiretap
+
+go 1.19
+
+replace golang.zx2c4.com/wireguard => github.com/luker983/wireguard-go v0.0.0-20221104205540-da3a7e2ca548
+
+//replace golang.zx2c4.com/wireguard => ../custom-wireguard-go
+
+require (
+ github.com/atotto/clipboard v0.1.4
+ github.com/fatih/color v1.13.0
+ github.com/go-ping/ping v1.1.0
+ github.com/google/gopacket v1.1.19
+ github.com/libp2p/go-reuseport v0.2.0
+ github.com/spf13/cobra v1.6.1
+ golang.org/x/net v0.2.0
+ golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c
+ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
+ gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5
+)
+
+require (
+ github.com/google/btree v1.1.2 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/inconshreveable/mousetrap v1.0.1 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.16 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ golang.org/x/crypto v0.1.0 // indirect
+ golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sys v0.2.0 // indirect
+ golang.org/x/time v0.1.0 // indirect
+ golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
+)
diff --git a/src/go.sum b/src/go.sum
new file mode 100644
index 0000000..41764da
--- /dev/null
+++ b/src/go.sum
@@ -0,0 +1,78 @@
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
+github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
+github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
+github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
+github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560=
+github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k=
+github.com/luker983/wireguard-go v0.0.0-20221104205540-da3a7e2ca548 h1:uW1Avhk2NAWOxGOMndkU99VNRkjmyIGpKAPqzMqWoN8=
+github.com/luker983/wireguard-go v0.0.0-20221104205540-da3a7e2ca548/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
+golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
+golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
+golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
+golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5 h1:cv/zaNV0nr1mJzaeo4S5mHIm5va1W0/9J3/5prlsuRM=
+gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5/go.mod h1:TIvkJD0sxe8pIob3p6T8IzxXunlp6yfgktvTNp+DGNM=
diff --git a/src/main.go b/src/main.go
new file mode 100644
index 0000000..ef06e10
--- /dev/null
+++ b/src/main.go
@@ -0,0 +1,9 @@
+package main
+
+import (
+ "wiretap/cmd"
+)
+
+func main() {
+ cmd.Execute()
+}
diff --git a/src/peer/config.go b/src/peer/config.go
new file mode 100644
index 0000000..d5ad247
--- /dev/null
+++ b/src/peer/config.go
@@ -0,0 +1,257 @@
+package peer
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net"
+ "strings"
+
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+)
+
+type Config struct {
+ config wgtypes.Config
+ peers []PeerConfig
+ addresses []net.IPNet
+}
+
+type configJSON struct {
+ Config wgtypes.Config
+ Peers []PeerConfig
+ Addresses []net.IPNet
+}
+
+type ConfigArgs struct {
+ PrivateKey string
+ ListenPort int
+ FirewallMark int
+ ReplacePeers bool
+ Peers []PeerConfigArgs
+ Addresses []string
+}
+
+func GetConfig(args ConfigArgs) (Config, error) {
+ c, err := NewConfig()
+ if err != nil {
+ return Config{}, err
+ }
+
+ if len(args.PrivateKey) != 0 {
+ err = c.SetPrivateKey(args.PrivateKey)
+ if err != nil {
+ return Config{}, err
+ }
+ }
+
+ if args.ListenPort != 0 {
+ err = c.SetPort(args.ListenPort)
+ if err != nil {
+ return Config{}, err
+ }
+ }
+
+ if args.FirewallMark != 0 {
+ err = c.SetFirewallMark(args.FirewallMark)
+ if err != nil {
+ return Config{}, err
+ }
+ }
+
+ c.SetReplacePeers(args.ReplacePeers)
+
+ for _, peer := range args.Peers {
+ newPeer, err := GetPeerConfig(peer)
+ if err != nil {
+ return Config{}, err
+ }
+
+ c.AddPeer(newPeer)
+ }
+
+ if len(args.Addresses) != 0 {
+ err = c.SetAddresses(args.Addresses)
+ if err != nil {
+ return Config{}, err
+ }
+ }
+
+ return c, nil
+}
+
+func NewConfig() (Config, error) {
+ privateKey, err := wgtypes.GeneratePrivateKey()
+ if err != nil {
+ return Config{}, err
+ }
+
+ return Config{
+ config: wgtypes.Config{
+ PrivateKey: &privateKey,
+ },
+ }, nil
+}
+
+func (c *Config) MarshalJSON() ([]byte, error) {
+ return json.Marshal(configJSON{
+ c.config,
+ c.peers,
+ c.addresses,
+ })
+}
+
+func (c *Config) UnmarshalJSON(b []byte) error {
+ tmp := &configJSON{}
+
+ err := json.Unmarshal(b, &tmp)
+ if err != nil {
+ return err
+ }
+
+ c.config = tmp.Config
+ c.peers = tmp.Peers
+ c.addresses = tmp.Addresses
+
+ return nil
+}
+
+func (c *Config) SetPrivateKey(privateKey string) error {
+ key, err := parseKey(privateKey)
+ if err != nil {
+ return err
+ }
+
+ c.config.PrivateKey = key
+ return nil
+}
+
+func (c *Config) GetPrivateKey() string {
+ return c.config.PrivateKey.String()
+}
+
+func (c *Config) SetPort(port int) error {
+ if port < 1 || port > 65535 {
+ return errors.New("invalid port")
+ }
+
+ c.config.ListenPort = &port
+ return nil
+}
+
+func (c *Config) SetFirewallMark(mark int) error {
+ if mark < 1 {
+ return errors.New("invalid firewall mark")
+ }
+
+ c.config.FirewallMark = &mark
+ return nil
+}
+
+func (c *Config) SetReplacePeers(replacePeers bool) {
+ c.config.ReplacePeers = replacePeers
+}
+
+func (c *Config) AddPeer(p PeerConfig) {
+ c.peers = append(c.peers, p)
+}
+
+func (c *Config) SetAddresses(addrs []string) error {
+ c.addresses = []net.IPNet{}
+ for _, a := range addrs {
+ // Ignore empty strings.
+ if len(a) == 0 {
+ continue
+ }
+
+ _, ipnet, err := net.ParseCIDR(a)
+ if err != nil {
+ return err
+ }
+
+ c.addresses = append(c.addresses, *ipnet)
+ }
+
+ return nil
+}
+
+func (c *Config) GetAddresses() []net.IPNet {
+ return c.addresses
+}
+
+func (c *Config) GetPublicKey() string {
+ return c.config.PrivateKey.PublicKey().String()
+}
+
+func (c *Config) GetPeers() []PeerConfig {
+ return c.peers
+}
+
+func (c *Config) GetPeerPrivateKey(i int) string {
+ if len(c.peers) > i {
+ if c.peers[i].privateKey != nil {
+ return c.peers[i].privateKey.String()
+ }
+ }
+
+ return ""
+}
+
+func (c *Config) GetPeerPublicKey(i int) string {
+ if len(c.peers) > i {
+ return c.peers[i].config.PublicKey.String()
+ }
+
+ return ""
+}
+
+func (c *Config) GetPeerEndpoint(i int) string {
+ if len(c.peers) > i {
+ endpoint := c.peers[i].config.Endpoint
+ if endpoint != nil {
+ return endpoint.String()
+ }
+
+ return ""
+ }
+
+ return ""
+}
+
+func (c *Config) AsFile() string {
+ var s strings.Builder
+
+ s.WriteString("[Interface]\n")
+ s.WriteString(fmt.Sprintf("PrivateKey = %s\n", c.config.PrivateKey.String()))
+ for _, a := range c.addresses {
+ s.WriteString(fmt.Sprintf("Address = %s\n", a.String()))
+ }
+ s.WriteString(fmt.Sprintf("ListenPort = %d\n", *c.config.ListenPort))
+ for _, p := range c.peers {
+ s.WriteString(fmt.Sprintf("\n%s", p.AsFile()))
+ }
+
+ return s.String()
+}
+
+func (c *Config) AsShareableFile() string {
+ var s strings.Builder
+
+ s.WriteString("[Peer]\n")
+ s.WriteString(fmt.Sprintf("PublicKey = %s\n", c.config.PrivateKey.PublicKey().String()))
+ s.WriteString("AllowedIPs = 0.0.0.0/32\n")
+
+ return s.String()
+}
+
+func (c *Config) AsIPC() string {
+ var s strings.Builder
+
+ s.WriteString(fmt.Sprintf("private_key=%s\n", hex.EncodeToString(c.config.PrivateKey[:])))
+ s.WriteString(fmt.Sprintf("listen_port=%d\n", *c.config.ListenPort))
+ for _, p := range c.peers {
+ s.WriteString(p.AsIPC())
+ }
+
+ return s.String()
+}
diff --git a/src/peer/peer.go b/src/peer/peer.go
new file mode 100644
index 0000000..e018cc4
--- /dev/null
+++ b/src/peer/peer.go
@@ -0,0 +1,33 @@
+// Package peer contains functions to perform configuration and validation operations on devices and peers.
+package peer
+
+import (
+ "encoding/base64"
+ "encoding/hex"
+
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+)
+
+func parseKey(key string) (*wgtypes.Key, error) {
+ var parsedKey wgtypes.Key
+ var err error
+
+ // Attempt to parse key.
+ parsedKey, err = wgtypes.ParseKey(key)
+ if err != nil {
+ // Attempt to parse as hex.
+ parseErr := err
+ keyBytes, err := hex.DecodeString(key)
+ if err != nil {
+ return nil, parseErr
+ }
+
+ encodedKey := base64.StdEncoding.EncodeToString(keyBytes)
+ parsedKey, err = wgtypes.ParseKey(encodedKey)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return &parsedKey, nil
+}
diff --git a/src/peer/peer_config.go b/src/peer/peer_config.go
new file mode 100644
index 0000000..21c5c87
--- /dev/null
+++ b/src/peer/peer_config.go
@@ -0,0 +1,243 @@
+package peer
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "net"
+ "strings"
+ "time"
+
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+)
+
+type PeerConfig struct {
+ config wgtypes.PeerConfig
+ privateKey *wgtypes.Key
+}
+
+type peerConfigJSON struct {
+ Config wgtypes.PeerConfig
+ PrivateKey *wgtypes.Key
+}
+
+type PeerConfigArgs struct {
+ PublicKey string
+ Remove bool
+ UpdateOnly bool
+ PresharedKey string
+ Endpoint string
+ PersistentKeepaliveInterval int
+ ReplaceAllowedIPs bool
+ AllowedIPs []string
+ PrivateKey string
+}
+
+func GetPeerConfig(args PeerConfigArgs) (PeerConfig, error) {
+ c, err := NewPeerConfig()
+ if err != nil {
+ return PeerConfig{}, err
+ }
+
+ if len(args.PublicKey) != 0 {
+ err = c.SetPublicKey(args.PublicKey)
+ if err != nil {
+ return PeerConfig{}, err
+ }
+ }
+
+ c.SetRemove(args.Remove)
+ c.SetUpdateOnly(args.UpdateOnly)
+
+ if len(args.PresharedKey) != 0 {
+ err = c.SetPresharedKey(args.PresharedKey)
+ if err != nil {
+ return PeerConfig{}, err
+ }
+ }
+
+ if len(args.Endpoint) != 0 {
+ err = c.SetEndpoint(args.Endpoint)
+ if err != nil {
+ return PeerConfig{}, err
+ }
+ }
+
+ if args.PersistentKeepaliveInterval != 0 {
+ err = c.SetPersistentKeepaliveInterval(args.PersistentKeepaliveInterval)
+ if err != nil {
+ return PeerConfig{}, err
+ }
+ }
+
+ c.SetReplaceAllowedIPs(args.ReplaceAllowedIPs)
+
+ err = c.SetAllowedIPs(args.AllowedIPs)
+ if err != nil {
+ return PeerConfig{}, err
+ }
+
+ if len(args.PrivateKey) != 0 {
+ err = c.SetPrivateKey(args.PrivateKey)
+ if err != nil {
+ return PeerConfig{}, err
+ }
+ }
+
+ return c, nil
+}
+
+func NewPeerConfig() (PeerConfig, error) {
+ privateKey, err := wgtypes.GeneratePrivateKey()
+ if err != nil {
+ return PeerConfig{}, err
+ }
+
+ return PeerConfig{
+ config: wgtypes.PeerConfig{
+ PublicKey: privateKey.PublicKey(),
+ },
+ privateKey: &privateKey,
+ }, nil
+}
+
+func (p *PeerConfig) MarshalJSON() ([]byte, error) {
+ return json.Marshal(peerConfigJSON{
+ p.config,
+ p.privateKey,
+ })
+}
+
+func (p *PeerConfig) UnmarshalJSON(b []byte) error {
+ tmp := &peerConfigJSON{}
+
+ err := json.Unmarshal(b, &tmp)
+ if err != nil {
+ return err
+ }
+
+ p.config = tmp.Config
+ p.privateKey = tmp.PrivateKey
+
+ return nil
+}
+
+func (p *PeerConfig) SetPublicKey(publicKey string) error {
+ key, err := parseKey(publicKey)
+ if err != nil {
+ return err
+ }
+
+ p.privateKey = nil
+ p.config.PublicKey = *key
+ return nil
+}
+
+func (p *PeerConfig) GetPublicKey() wgtypes.Key {
+ return p.config.PublicKey
+}
+
+func (p *PeerConfig) SetRemove(remove bool) {
+ p.config.Remove = remove
+}
+
+func (p *PeerConfig) SetUpdateOnly(updateOnly bool) {
+ p.config.UpdateOnly = updateOnly
+}
+
+func (p *PeerConfig) SetPresharedKey(presharedKey string) error {
+ key, err := parseKey(presharedKey)
+ if err != nil {
+ return err
+ }
+
+ p.config.PresharedKey = key
+ return nil
+}
+
+func (p *PeerConfig) SetEndpoint(addr string) error {
+ endpoint, err := net.ResolveUDPAddr("udp", addr)
+ if err != nil {
+ return err
+ }
+
+ p.config.Endpoint = endpoint
+ return nil
+}
+
+func (p *PeerConfig) SetPersistentKeepaliveInterval(keepalive int) error {
+ secs, err := time.ParseDuration(fmt.Sprintf("%ds", keepalive))
+ if err != nil {
+ return err
+ }
+
+ p.config.PersistentKeepaliveInterval = &secs
+ return nil
+}
+
+func (p *PeerConfig) SetReplaceAllowedIPs(replaceAllowedIPs bool) {
+ p.config.ReplaceAllowedIPs = replaceAllowedIPs
+}
+
+func (p *PeerConfig) SetAllowedIPs(allowedIPs []string) error {
+ for _, a := range allowedIPs {
+ // Skip empty allowed IPs
+ if len(a) == 0 {
+ continue
+ }
+ _, ipnet, err := net.ParseCIDR(a)
+ if err != nil {
+ return err
+ }
+
+ p.config.AllowedIPs = append(p.config.AllowedIPs, *ipnet)
+ }
+
+ return nil
+}
+
+func (p *PeerConfig) GetAllowedIPs() []net.IPNet {
+ return p.config.AllowedIPs
+}
+
+func (p *PeerConfig) SetPrivateKey(privateKey string) error {
+ key, err := parseKey(privateKey)
+ if err != nil {
+ return err
+ }
+
+ p.privateKey = key
+ p.config.PublicKey = key.PublicKey()
+ return nil
+}
+
+func (p *PeerConfig) AsFile() string {
+ var s strings.Builder
+
+ s.WriteString("[Peer]\n")
+ s.WriteString(fmt.Sprintf("PublicKey = %s\n", p.config.PublicKey.String()))
+ ips := []string{}
+ for _, a := range p.config.AllowedIPs {
+ ips = append(ips, a.String())
+ }
+ s.WriteString(fmt.Sprintf("AllowedIPs = %s\n", strings.Join(ips, ",")))
+
+ return s.String()
+}
+
+func (p *PeerConfig) AsIPC() string {
+ var s strings.Builder
+
+ s.WriteString(fmt.Sprintf("public_key=%s\n", hex.EncodeToString(p.config.PublicKey[:])))
+ if p.config.Endpoint != nil {
+ s.WriteString(fmt.Sprintf("endpoint=%s\n", p.config.Endpoint.String()))
+ }
+ for _, a := range p.config.AllowedIPs {
+ s.WriteString(fmt.Sprintf("allowed_ip=%s\n", a.String()))
+ }
+ if p.config.PersistentKeepaliveInterval != nil {
+ s.WriteString(fmt.Sprintf("persistent_keepalive_interval=%.0f\n", p.config.PersistentKeepaliveInterval.Seconds()))
+ }
+
+ return s.String()
+}
diff --git a/src/transport/api/api.go b/src/transport/api/api.go
new file mode 100644
index 0000000..8ddf631
--- /dev/null
+++ b/src/transport/api/api.go
@@ -0,0 +1,222 @@
+// Package API handles the internal API running on the server.
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "net/netip"
+ "sync"
+
+ "golang.zx2c4.com/wireguard/device"
+ "golang.zx2c4.com/wireguard/tun/netstack"
+ "gvisor.dev/gvisor/pkg/tcpip"
+ "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
+ "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
+ "gvisor.dev/gvisor/pkg/tcpip/stack"
+ "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
+
+ "wiretap/peer"
+ "wiretap/transport"
+)
+
+// Handle adds rule to top of firewall rules that accepts direct connections to API.
+func Handle(tnet *netstack.Net, dev *device.Device, config *peer.Config, addr netip.Addr, port uint16, lock *sync.Mutex) {
+ s := tnet.Stack()
+
+ headerFilter := stack.IPHeaderFilter{
+ Protocol: tcp.ProtocolNumber,
+ CheckProtocol: true,
+ Dst: tcpip.Address(addr.AsSlice()),
+ DstMask: tcpip.Address(bytes.Repeat([]byte("\xff"), addr.BitLen()/8)),
+ }
+
+ rule := stack.Rule{
+ Filter: headerFilter,
+ Target: &stack.AcceptTarget{
+ NetworkProtocol: func() tcpip.NetworkProtocolNumber {
+ if addr.Is4() {
+ return ipv4.ProtocolNumber
+ }
+ return ipv6.ProtocolNumber
+ }(),
+ },
+ }
+
+ tid := stack.NATID
+ transport.PushRule(s, rule, tid, addr.Is6())
+ lock.Unlock()
+
+ // Stand up API server.
+ listener, err := tnet.ListenTCP(&net.TCPAddr{IP: addr.AsSlice(), Port: int(port)})
+ if err != nil {
+ log.Panic(err)
+ }
+
+ http.HandleFunc("/ping", wrapApi(handlePing()))
+ http.HandleFunc("/serverinfo", wrapApi(handleServerInfo(config)))
+ http.HandleFunc("/peers/add", wrapApi(handlePeerAdd(dev, config)))
+
+ log.Println("API: API listener up")
+ err = http.Serve(listener, nil)
+ if err != nil {
+ log.Panic(err)
+ }
+}
+
+// wrapAPI logs all API requests.
+func wrapApi(f http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ log.Printf("(client %s) - API: %s", r.RemoteAddr, r.RequestURI)
+ f(w, r)
+ }
+}
+
+func writeErr(w http.ResponseWriter, err error) {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, err = io.WriteString(w, err.Error())
+ if err != nil {
+ log.Printf("API Error: %v", err)
+ }
+}
+
+func handlePing() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ _, err := io.WriteString(w, "pong")
+ if err != nil {
+ log.Printf("API Error: %v", err)
+ }
+ }
+}
+
+func handleServerInfo(config *peer.Config) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ body, err := json.Marshal(config)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ _, err = w.Write(body)
+ if err != nil {
+ log.Printf("API Error: %v", err)
+ }
+ }
+}
+
+func handlePeerAdd(dev *device.Device, config *peer.Config) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ w.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ var p peer.PeerConfig
+ err := json.NewDecoder(r.Body).Decode(&p)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ // If addresses not assigned, choose new address dynamically.
+ var newAddrs []string
+ peerAddrs := p.GetAllowedIPs()
+ serverAddrs := config.GetAddresses()
+ if len(peerAddrs) == 0 {
+ if len(config.GetAddresses()) < 2 {
+ writeErr(w, errors.New("unable to dynamically assign addresses"))
+ }
+
+ prefix4, err := netip.ParsePrefix(serverAddrs[0].String())
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ prefix6, err := netip.ParsePrefix(serverAddrs[1].String())
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ zeroAddr := netip.Addr{}
+ availableAddr4 := findAvailableAddr(config, prefix4.Addr())
+ if availableAddr4 != zeroAddr {
+ prefix4, _ = availableAddr4.Prefix(availableAddr4.BitLen())
+ newAddrs = append(newAddrs, prefix4.String())
+ }
+ availableAddr6 := findAvailableAddr(config, prefix6.Addr())
+ if availableAddr6 != zeroAddr {
+ prefix6, _ = availableAddr6.Prefix(availableAddr6.BitLen())
+ newAddrs = append(newAddrs, prefix6.String())
+ }
+
+ err = p.SetAllowedIPs(newAddrs)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+ }
+
+ if len(p.GetAllowedIPs()) == 0 {
+ writeErr(w, errors.New("no addresses"))
+ return
+ }
+
+ fmt.Println()
+ fmt.Println(p.AsIPC())
+
+ err = dev.IpcSet(p.AsIPC())
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ log.Printf("API: Peer Added: %s", p.GetPublicKey().String())
+
+ body, err := json.Marshal(&p)
+ if err != nil {
+ writeErr(w, err)
+ return
+ }
+
+ config.AddPeer(p)
+
+ _, err = w.Write(body)
+ if err != nil {
+ log.Printf("API Error: %v", err)
+ }
+ }
+}
+
+func findAvailableAddr(config *peer.Config, baseAddr netip.Addr) netip.Addr {
+ zeroAddr := netip.Addr{}
+ candidate := baseAddr.Next()
+ // Loop until address is found or zero address hit.
+CandidateLoop:
+ for candidate != zeroAddr {
+ for _, peer := range config.GetPeers() {
+ for _, aip := range peer.GetAllowedIPs() {
+ // Already in use.
+ if netip.MustParsePrefix(aip.String()).Addr() == candidate {
+ candidate = candidate.Next()
+ continue CandidateLoop
+ }
+ }
+ }
+
+ return candidate
+ }
+
+ return zeroAddr
+}
diff --git a/src/transport/icmp/icmp.go b/src/transport/icmp/icmp.go
new file mode 100644
index 0000000..c21c9d4
--- /dev/null
+++ b/src/transport/icmp/icmp.go
@@ -0,0 +1,223 @@
+// Package icmp handles ICMPv4 and ICMPv6 messages.
+package icmp
+
+import (
+ "log"
+ "net"
+ "net/netip"
+ "sync"
+
+ neticmp "golang.org/x/net/icmp"
+ netipv4 "golang.org/x/net/ipv4"
+ netipv6 "golang.org/x/net/ipv6"
+
+ "golang.zx2c4.com/wireguard/tun/netstack"
+ "gvisor.dev/gvisor/pkg/tcpip"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
+ "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
+ "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
+ "gvisor.dev/gvisor/pkg/tcpip/stack"
+ "gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
+
+ "wiretap/transport"
+)
+
+// preroutingMatch matches packets in the prerouting stage and clones:
+// packet into channel for processing.
+type preroutingMatch struct {
+ pktChan chan *stack.PacketBuffer
+}
+
+var pinger Ping = nil
+
+// When a new ICMP message hits the prerouting stage, the packet is cloned
+// to the ICMP handler and dropped here.
+func (m preroutingMatch) Match(hook stack.Hook, packet *stack.PacketBuffer, inputInterfaceName, outputInterfaceName string) (matches bool, hotdrop bool) {
+ if hook == stack.Prerouting {
+ m.pktChan <- packet.Clone()
+ return false, true
+ }
+
+ return false, false
+}
+
+// handleICMP proxies ICMP messages using whatever means it can with the permissions this binary
+// has on the system.
+func Handle(tnet *netstack.Net, lock *sync.Mutex) {
+ s := tnet.Stack()
+
+ // create iptables rule that drops icmp, but clones packet and sends it to this handler.
+ headerFilter4 := stack.IPHeaderFilter{
+ Protocol: icmp.ProtocolNumber4,
+ CheckProtocol: true,
+ }
+
+ headerFilter6 := stack.IPHeaderFilter{
+ Protocol: icmp.ProtocolNumber6,
+ CheckProtocol: true,
+ }
+
+ match := preroutingMatch{
+ pktChan: make(chan *stack.PacketBuffer),
+ }
+
+ rule4 := stack.Rule{
+ Filter: headerFilter4,
+ Matchers: []stack.Matcher{match},
+ Target: &stack.DropTarget{
+ NetworkProtocol: ipv4.ProtocolNumber,
+ },
+ }
+
+ rule6 := stack.Rule{
+ Filter: headerFilter6,
+ Matchers: []stack.Matcher{match},
+ Target: &stack.DropTarget{
+ NetworkProtocol: ipv6.ProtocolNumber,
+ },
+ }
+
+ tid := stack.NATID
+ transport.PushRule(s, rule4, tid, false)
+ transport.PushRule(s, rule6, tid, true)
+ lock.Unlock()
+
+ log.Println("Transport: ICMP listener up")
+ for {
+ clonedPacket := <-match.pktChan
+ go func() {
+ handleMessage(s, clonedPacket)
+ clonedPacket.DecRef()
+ }()
+ }
+}
+
+// handleICMPMessage parses ICMP packets and proxies them if possible.
+func handleMessage(s *stack.Stack, packet *stack.PacketBuffer) {
+ // Parse ICMP packet type.
+ netHeader := packet.Network()
+ log.Printf("(client %v) - Transport: ICMP -> %v", netHeader.SourceAddress(), netHeader.DestinationAddress())
+
+ isIpv6 := !netip.MustParseAddr(netHeader.SourceAddress().String()).Is4()
+ if isIpv6 {
+ transHeader := header.ICMPv6(netHeader.Payload())
+ switch transHeader.Type() {
+ case header.ICMPv6EchoRequest:
+ handleEcho(s, packet)
+ default:
+ log.Println("ICMPv6 type not implemented:", transHeader.Type())
+ }
+ } else {
+ transHeader := header.ICMPv4(netHeader.Payload())
+ switch transHeader.Type() {
+ case header.ICMPv4Echo:
+ handleEcho(s, packet)
+ default:
+ log.Println("ICMPv4 type not implemented:", transHeader.Type())
+ }
+ }
+
+}
+
+// handleICMPEcho tries to send ICMP echo requests to the true destination however it can.
+// If successful, it sends an echo response to the peer.
+func handleEcho(s *stack.Stack, packet *stack.PacketBuffer) {
+ var success bool
+ var err error
+
+ // Parse network header for destination address.
+ dest := packet.Network().DestinationAddress().String()
+
+ if pinger == nil {
+ pinger, success, err = getPing(dest)
+ } else {
+ success, err = pinger.ping(dest)
+ }
+
+ if err == nil {
+ if success {
+ sendEchoResponse(s, packet)
+ }
+
+ return
+ }
+
+ log.Printf("ping failed: %v", err)
+}
+
+// sendICMPEchoResponse sends an echo response to the peer with a spoofed source address.
+func sendEchoResponse(s *stack.Stack, packet *stack.PacketBuffer) {
+ var response []byte
+ var ipHeader []byte
+ var err error
+
+ netHeader := packet.Network()
+
+ isIpv6 := netHeader.DestinationAddress().To4() == ""
+
+ netProto := ipv4.ProtocolNumber
+ if isIpv6 {
+ netProto = ipv6.ProtocolNumber
+ transHeader := header.ICMPv6(netHeader.Payload())
+ // Create ICMP response and marshal it.
+ response, err = (&neticmp.Message{
+ Type: netipv6.ICMPTypeEchoReply,
+ Code: 0,
+ Body: &neticmp.Echo{
+ ID: int(transHeader.Ident()),
+ Seq: int(transHeader.Sequence()),
+ Data: transHeader.Payload(),
+ },
+ }).Marshal(neticmp.IPv6PseudoHeader(net.ParseIP(netHeader.DestinationAddress().String()), net.ParseIP(netHeader.SourceAddress().String())))
+ if err != nil {
+ log.Println("Failed to marshal response:", err)
+ return
+ }
+
+ // Assert type to get network header bytes.
+ ipv6Header, ok := netHeader.(header.IPv6)
+ if !ok {
+ log.Println("Could not assert network header as IPv6 header")
+ return
+ }
+ // Swap source and destination addresses from original request.
+ tmp := ipv6Header.DestinationAddress()
+ ipv6Header.SetDestinationAddress(ipv6Header.SourceAddress())
+ ipv6Header.SetSourceAddress(tmp)
+ ipHeader = ipv6Header
+ } else {
+ transHeader := header.ICMPv4(netHeader.Payload())
+ // Create ICMP response and marshal it.
+ response, err = (&neticmp.Message{
+ Type: netipv4.ICMPTypeEchoReply,
+ Code: 0,
+ Body: &neticmp.Echo{
+ ID: int(transHeader.Ident()),
+ Seq: int(transHeader.Sequence()),
+ Data: transHeader.Payload(),
+ },
+ }).Marshal(nil)
+ if err != nil {
+ log.Println("Failed to marshal response:", err)
+ return
+ }
+
+ // Assert type to get network header bytes.
+ ipv4Header, ok := netHeader.(header.IPv4)
+ if !ok {
+ log.Println("Could not assert network header as IPv4 header")
+ return
+ }
+ // Swap source and destination addresses from original request.
+ tmp := ipv4Header.DestinationAddress()
+ ipv4Header.SetDestinationAddress(ipv4Header.SourceAddress())
+ ipv4Header.SetSourceAddress(tmp)
+ ipHeader = ipv4Header
+ }
+
+ tcpipErr := transport.SendPacket(s, append(ipHeader, response...), &tcpip.FullAddress{NIC: 1, Addr: netHeader.SourceAddress()}, netProto)
+ if tcpipErr != nil {
+ log.Println("Failed to write:", tcpipErr)
+ return
+ }
+}
diff --git a/src/transport/icmp/ping.go b/src/transport/icmp/ping.go
new file mode 100644
index 0000000..31a473a
--- /dev/null
+++ b/src/transport/icmp/ping.go
@@ -0,0 +1,87 @@
+package icmp
+
+import (
+ "log"
+ "os/exec"
+ "runtime"
+
+ "github.com/go-ping/ping"
+)
+
+// Ping type is an interface for the different methods of performing an unpriveleged ICMP echo request.
+type Ping interface {
+ ping(string) (bool, error)
+}
+
+type socketPing struct{}
+type execPing struct{}
+type noPing struct{}
+
+func getPing(addr string) (pinger Ping, success bool, err error) {
+ pingers := []Ping{socketPing{}, execPing{}, noPing{}}
+
+ for _, p := range pingers {
+ s, err := p.ping(addr)
+ if err != nil {
+ log.Printf("ping method failed: %v", err)
+ continue
+ }
+
+ return p, s, err
+ }
+
+ return noPing{}, false, nil
+}
+
+// socketPing attempts to ping destination address via socket. Only some systems
+// will allow an unprivileged user to do this.
+func (socketPing) ping(addr string) (success bool, err error) {
+ pinger, err := ping.NewPinger(addr)
+ if err != nil {
+ return false, err
+ }
+
+ pinger.Count = 1
+ err = pinger.Run()
+ if err != nil {
+ return false, err
+ }
+
+ return true, nil
+}
+
+// execPing attempts to ping destination address via ping binary on the local machine.
+// This is a last resort used if unable to open ICMP echo socket.
+func (execPing) ping(addr string) (success bool, err error) {
+ pingPath, err := exec.LookPath("ping")
+ if err != nil {
+ return false, err
+ }
+
+ // ping -count 1 -timeout 1000ms
+ // Use platform-specific args.
+ var args []string
+ switch runtime.GOOS {
+ case "windows":
+ args = []string{pingPath, "-n", "1", "-w", "1000", addr}
+ default:
+ args = []string{pingPath, "-c", "1", "-t", "1", addr}
+ }
+
+ cmd := &exec.Cmd{
+ Path: pingPath,
+ Args: args,
+ }
+
+ err = cmd.Run()
+ if err != nil {
+ return false, err
+ }
+
+ return true, nil
+}
+
+// noPing is a placeholder for when no supported ping method exists on the remote machine.
+func (noPing) ping(addr string) (success bool, err error) {
+ return false, nil
+}
diff --git a/src/transport/tcp/tcp.go b/src/transport/tcp/tcp.go
new file mode 100644
index 0000000..eebed30
--- /dev/null
+++ b/src/transport/tcp/tcp.go
@@ -0,0 +1,403 @@
+// Package tcp proxies TCP connections between a WireGuard peer and a destination
+// accessible by the machine where Wiretap is running.
+package tcp
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "os"
+ "sync"
+ "syscall"
+
+ "net/netip"
+
+ "github.com/google/gopacket"
+ "github.com/google/gopacket/layers"
+
+ "golang.zx2c4.com/wireguard/tun/netstack"
+ "gvisor.dev/gvisor/pkg/tcpip"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
+ "gvisor.dev/gvisor/pkg/tcpip/link/channel"
+ "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
+ "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
+ "gvisor.dev/gvisor/pkg/tcpip/stack"
+ "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
+
+ "wiretap/transport"
+)
+
+// tcpConn tracks a connection, source and destination IP and Port.
+type tcpConn struct {
+ Source string
+ Dest string
+}
+
+// connTrack holds the net.Conn to a final destination
+// and the status of that connection.
+type connTrack struct {
+ Connecting bool
+ Conn net.Conn
+}
+
+// Keep track of connections so we don't duplicate work.
+var isOpen = make(map[tcpConn]connTrack)
+var isOpenLock = sync.RWMutex{}
+
+// preroutingMatch matches packets in the prerouting stage.
+type preroutingMatch struct {
+ pktChan chan *stack.PacketBuffer
+ endpoint *channel.Endpoint
+}
+
+// Match looks for SYN packets (start of a tcp conn). Before proxying connection, we need to check
+// if intendend destination is up. Drop the packet to prevent blocking, but start goroutine that
+// connects to destination. If destination is up, reinject packet and allow it through.
+func (m preroutingMatch) Match(hook stack.Hook, packet *stack.PacketBuffer, inputInterfaceName, outputInterfaceName string) (matches bool, hotdrop bool) {
+ if hook == stack.Prerouting {
+ // If SYN flag set, see if connection possible.
+ netHeader := packet.Network()
+ transHeader := header.TCP(netHeader.Payload())
+
+ flags := transHeader.Flags()
+ if flags.Contains(header.TCPFlagSyn) && !flags.Contains(header.TCPFlagAck) {
+ dest := net.JoinHostPort(netHeader.DestinationAddress().String(), fmt.Sprint(transHeader.DestinationPort()))
+ source := net.JoinHostPort(netHeader.SourceAddress().String(), fmt.Sprint(transHeader.SourcePort()))
+ c := tcpConn{source, dest}
+
+ isOpenLock.RLock()
+ ctrack, ok := isOpen[c]
+ isOpenLock.RUnlock()
+
+ // If not in conn map, drop this packet for now, but clone so it can
+ // be reinjected if connections are successful.
+ if !ok {
+ isOpenLock.Lock()
+ // In progress, but not ready to forward SYN packets yet.
+ isOpen[c] = connTrack{
+ Connecting: true,
+ }
+ isOpenLock.Unlock()
+
+ packetClone := packet.Clone()
+ go func() {
+ checkIfOpen(c, m.pktChan, packetClone, m.endpoint)
+ packetClone.DecRef()
+ }()
+
+ // Hotdrop because we're taking control of the packet.
+ return false, true
+ // Already checking if port is open. Do nothing.
+ } else if ctrack.Connecting {
+ return false, false
+ // Connection is verified to be open. Allow this connection and reset conn map.
+ } else {
+ return true, false
+ }
+ }
+ // ACK here means ACK without prior connection, drop.
+ if transHeader.Flags() == header.TCPFlagAck {
+ return false, false
+ }
+ }
+
+ return false, false
+}
+
+// If destination is open, whitelist and reinject. Otherwise send reset.
+func checkIfOpen(conn tcpConn, pktChan chan *stack.PacketBuffer, packet *stack.PacketBuffer, endpoint *channel.Endpoint) {
+ log.Printf("(client %v) - Transport: TCP -> %v", conn.Source, conn.Dest)
+ c, err := net.Dial("tcp", conn.Dest)
+ if err != nil {
+ //log.Printf("Error connecting to %s: %s\n", conn.Dest, err)
+
+ // If connection refused, we can send a reset to let peer know.
+ if oerr, ok := err.(*net.OpError); ok {
+ if syserr, ok := oerr.Err.(*os.SyscallError); ok {
+ if syserr.Err == syscall.ECONNREFUSED {
+ //log.Println("Connection refused, sending reset")
+ pktChan <- packet.Clone()
+ }
+ }
+ }
+
+ // Error, reset connection progress.
+ isOpenLock.Lock()
+ delete(isOpen, conn)
+ isOpenLock.Unlock()
+ return
+ }
+
+ // No error, mark successful and reinject packet.
+ isOpenLock.Lock()
+ isOpen[conn] = connTrack{
+ Connecting: false,
+ Conn: c,
+ }
+ isOpenLock.Unlock()
+
+ isIpv6 := !netip.MustParseAddrPort(c.RemoteAddr().String()).Addr().Is4()
+ netProto := ipv4.ProtocolNumber
+ if isIpv6 {
+ netProto = ipv6.ProtocolNumber
+ }
+ new_packet := stack.NewPacketBuffer(stack.PacketBufferOptions{
+ Payload: packet.ToBuffer(),
+ })
+ endpoint.InjectInbound(netProto, new_packet)
+}
+
+// Handle creates a DNAT rule that forwards destination packets to a tcp listener.
+// Once a connection is accepted, it gets handed off to handleConn().
+func Handle(tnet *netstack.Net, ipv4Addr netip.Addr, ipv6Addr netip.Addr, port uint16, lock *sync.Mutex) {
+ s := tnet.Stack()
+
+ // Create iptables rule.
+ // iptables -t nat -A PREROUTING -p tcp -j DNAT --to-destination 192.168.0.1:80
+ headerFilter := stack.IPHeaderFilter{Protocol: tcp.ProtocolNumber,
+ CheckProtocol: true,
+ }
+
+ match := preroutingMatch{
+ pktChan: make(chan *stack.PacketBuffer, 1),
+ endpoint: tnet.Endpoint(),
+ }
+
+ rule4 := stack.Rule{
+ Filter: headerFilter,
+ Matchers: []stack.Matcher{match},
+ Target: &stack.DNATTarget{
+ Addr: tcpip.Address(ipv4Addr.AsSlice()),
+ Port: port,
+ NetworkProtocol: ipv4.ProtocolNumber,
+ },
+ }
+
+ rule6 := stack.Rule{
+ Filter: headerFilter,
+ Matchers: []stack.Matcher{match},
+ Target: &stack.DNATTarget{
+ Addr: tcpip.Address(ipv6Addr.AsSlice()),
+ Port: port,
+ NetworkProtocol: ipv6.ProtocolNumber,
+ },
+ }
+
+ tid := stack.NATID
+ transport.PushRule(s, rule4, tid, false)
+ transport.PushRule(s, rule6, tid, true)
+ lock.Unlock()
+
+ // RST handler
+ go func() {
+ for {
+ packetClone := <-match.pktChan
+ go func() {
+ sendRST(s, packetClone)
+ packetClone.DecRef()
+ }()
+ }
+ }()
+
+ go startListener(tnet, s.IPTables(), &net.TCPAddr{Port: int(port)}, ipv4Addr, ipv6Addr)
+}
+
+// startListener accepts connections from WireGuard peer.
+func startListener(tnet *netstack.Net, tables *stack.IPTables, listenAddr *net.TCPAddr, localAddr4 netip.Addr, localAddr6 netip.Addr) {
+ l, err := tnet.ListenTCP(listenAddr)
+ if err != nil {
+ log.Panic(err)
+ }
+
+ defer l.Close()
+
+ log.Println("Transport: TCP listener up")
+ for {
+ // Every TCP connection gets accepted here.
+ c, err := l.Accept()
+ if err != nil {
+ log.Println("Failed to accept connection:", err)
+ continue
+ }
+
+ // Remote Address isn't populated yet.
+ // TODO: Figure out why and get rid of this silly busy loop.
+ go func() {
+ for {
+ if c.RemoteAddr() != nil {
+ break
+ }
+ }
+
+ isIpv6 := !netip.MustParseAddrPort(c.RemoteAddr().String()).Addr().Is4()
+ netProto := ipv4.ProtocolNumber
+ localAddr := localAddr4
+ if isIpv6 {
+ netProto = ipv6.ProtocolNumber
+ localAddr = localAddr6
+ }
+
+ handleConn(c, localAddr, netProto, tables)
+ }()
+ }
+
+}
+
+// handleConn finds the intended target of a peer's connection,
+// connects to that target, then copies data between the two.
+func handleConn(c net.Conn, ipAddr netip.Addr, netProto tcpip.NetworkProtocolNumber, tables *stack.IPTables) {
+ var wg sync.WaitGroup
+ defer c.Close()
+
+ // Lookup original destination of this connection.
+ remoteAddr := c.RemoteAddr()
+
+ if remoteAddr == nil {
+ log.Println("Could not read remote address of connection")
+ return
+ }
+ addr, port, tcpipErr := tables.OriginalDst(stack.TransportEndpointID{
+ LocalPort: 1337,
+ LocalAddress: tcpip.Address(ipAddr.AsSlice()), RemotePort: netip.MustParseAddrPort(remoteAddr.String()).Port(),
+ RemoteAddress: tcpip.Address(netip.MustParseAddrPort(remoteAddr.String()).Addr().AsSlice()),
+ }, netProto, tcp.ProtocolNumber)
+ if tcpipErr != nil {
+ log.Println("Error reading original destination:", tcpipErr)
+ return
+ }
+
+ dest := net.JoinHostPort(addr.String(), fmt.Sprint(port))
+ source := remoteAddr.String()
+ cString := tcpConn{source, dest}
+
+ // Original destination should be dialed already for when we checked if it was open:
+ isOpenLock.Lock()
+ ctrack, ok := isOpen[cString]
+ isOpenLock.Unlock()
+ if !ok {
+ log.Printf("Error looking up conn to destination: %v\n", net.JoinHostPort(addr.String(), fmt.Sprint(port)))
+ return
+ }
+
+ // Delete original destination from map so it can be remade.
+ newConn := ctrack.Conn
+ isOpenLock.Lock()
+ delete(isOpen, cString)
+ isOpenLock.Unlock()
+
+ // Copy from new connection to peer
+ wg.Add(1)
+ go func() {
+ //io.Copy(io.MultiWriter(c, os.Stdout), newConn)
+ _, err := io.Copy(c, newConn)
+ if err != nil {
+ log.Printf("Error copying between connections: %v\n", err)
+ }
+ wg.Done()
+ c.Close()
+ }()
+
+ // Copy from peer to new connection.
+ //io.Copy(io.MultiWriter(newConn, os.Stdout), c)
+ _, err := io.Copy(newConn, c)
+ if err != nil {
+ log.Printf("Error copying between connections: %v\n", err)
+ }
+ newConn.Close()
+
+ // Wait for both copies to finish.
+ wg.Wait()
+}
+
+// sendICMPEchoResponse sends an echo response to the peer with a spoofed source address.
+func sendRST(s *stack.Stack, packet *stack.PacketBuffer) {
+ var err error
+ var ipv4Layer *layers.IPv4
+ var ipv6Layer *layers.IPv6
+
+ netHeader := packet.Network()
+ transHeader := header.TCP(netHeader.Payload())
+
+ isIpv6 := netHeader.DestinationAddress().To4() == ""
+
+ if isIpv6 {
+ ipv6Layer = &layers.IPv6{}
+ ipv6Layer, err = transport.GetNetworkLayer[header.IPv6](netHeader, ipv6Layer)
+ ipv6Layer.SrcIP, ipv6Layer.DstIP = ipv6Layer.DstIP, ipv6Layer.SrcIP
+ } else {
+ ipv4Layer = &layers.IPv4{}
+ ipv4Layer, err = transport.GetNetworkLayer[header.IPv4](netHeader, ipv4Layer)
+ ipv4Layer.SrcIP, ipv4Layer.DstIP = ipv4Layer.DstIP, ipv4Layer.SrcIP
+ }
+
+ if err != nil {
+ log.Println("Could not decode Network header:", err)
+ return
+ }
+
+ // Create transport layer and swap ports, fix flags.
+ tcpLayer := &layers.TCP{}
+ err = tcpLayer.DecodeFromBytes(transHeader, gopacket.NilDecodeFeedback)
+ if err != nil {
+ log.Println("Could not decode TCP header:", err)
+ return
+ }
+
+ tcpLayer.SrcPort, tcpLayer.DstPort = tcpLayer.DstPort, tcpLayer.SrcPort
+ tcpLayer.Ack = tcpLayer.Seq + 1
+ tcpLayer.Seq = 0
+ tcpLayer.DataOffset = 5
+ tcpLayer.SYN = false
+ tcpLayer.RST = true
+ tcpLayer.ACK = true
+ tcpLayer.Window = 0
+ tcpLayer.Options = nil
+ tcpLayer.Padding = nil
+
+ if isIpv6 {
+ err = tcpLayer.SetNetworkLayerForChecksum(ipv6Layer)
+ } else {
+ err = tcpLayer.SetNetworkLayerForChecksum(ipv4Layer)
+ }
+
+ if err != nil {
+ log.Println("Could not set layer for checksum:", err)
+ return
+ }
+
+ buf := gopacket.NewSerializeBuffer()
+ options := gopacket.SerializeOptions{
+ ComputeChecksums: true,
+ FixLengths: true,
+ }
+ if isIpv6 {
+ err = gopacket.SerializeLayers(buf, options,
+ ipv6Layer,
+ tcpLayer,
+ )
+ } else {
+ err = gopacket.SerializeLayers(buf, options,
+ ipv4Layer,
+ tcpLayer,
+ )
+ }
+ if err != nil {
+ log.Println("Failed to serialize layers:", err)
+ return
+ }
+
+ response := buf.Bytes()
+
+ // Create network layer endpoint for spoofing source address.
+ proto := ipv4.ProtocolNumber
+ if isIpv6 {
+ proto = ipv6.ProtocolNumber
+ }
+
+ tcpipErr := transport.SendPacket(s, response, &tcpip.FullAddress{NIC: 1, Addr: netHeader.SourceAddress()}, proto)
+ if tcpipErr != nil {
+ log.Println("Failed to send reset:", tcpipErr)
+ return
+ }
+}
diff --git a/src/transport/transport.go b/src/transport/transport.go
new file mode 100644
index 0000000..012d0f1
--- /dev/null
+++ b/src/transport/transport.go
@@ -0,0 +1,67 @@
+// Package transport provides utility functions needed by all transport methods.
+package transport
+
+import (
+ "bytes"
+ "errors"
+
+ "github.com/google/gopacket"
+ "gvisor.dev/gvisor/pkg/tcpip"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
+ "gvisor.dev/gvisor/pkg/tcpip/stack"
+ "gvisor.dev/gvisor/pkg/waiter"
+)
+
+// PushRule pushes a rule onto a firewall table.
+func PushRule(s *stack.Stack, rule stack.Rule, tableId stack.TableID, ipv6 bool) {
+ table := s.IPTables().GetTable(tableId, ipv6)
+ table.Rules = append([]stack.Rule{rule}, table.Rules...)
+ s.IPTables().ReplaceTable(tableId, table, ipv6)
+}
+
+// IPHeader is a type interface used by GetNetworkLayer.
+type IPHeader interface {
+ header.IPv4 | header.IPv6
+}
+
+// IPLayer must have DecodeFromBytes function.
+type IPLayer interface {
+ DecodeFromBytes(data []byte, df gopacket.DecodeFeedback) error
+}
+
+// GetNetworkLayer parses a network header, then converts it to bytes.
+func GetNetworkLayer[H IPHeader, L IPLayer](netHeader header.Network, ipLayer L) (L, error) {
+ h, ok := netHeader.(H)
+ if !ok {
+ return ipLayer, errors.New("Could not assert network header as provided type")
+ }
+
+ err := ipLayer.DecodeFromBytes(h, gopacket.NilDecodeFeedback)
+ if err != nil {
+ return ipLayer, err
+ }
+
+ return ipLayer, nil
+}
+
+// SendPacket sends a network-layer packet.
+func SendPacket(s *stack.Stack, packet []byte, addr *tcpip.FullAddress, netProto tcpip.NetworkProtocolNumber) tcpip.Error {
+ // Create network layer endpoint for spoofing source address.
+ var wq waiter.Queue
+ ep, tcpipErr := s.NewPacketEndpoint(true, netProto, &wq)
+ if tcpipErr != nil {
+ return tcpipErr
+ }
+ defer ep.Close()
+
+ // Send packet.
+ buf := bytes.NewReader(packet)
+ _, tcpipErr = ep.Write(buf, tcpip.WriteOptions{
+ To: addr,
+ })
+ if tcpipErr != nil {
+ return tcpipErr
+ }
+
+ return nil
+}
diff --git a/src/transport/udp/udp.go b/src/transport/udp/udp.go
new file mode 100644
index 0000000..f4aff84
--- /dev/null
+++ b/src/transport/udp/udp.go
@@ -0,0 +1,495 @@
+package udp
+
+import (
+ "fmt"
+ "log"
+ "net"
+ "net/netip"
+ "os"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/google/gopacket"
+ "github.com/google/gopacket/layers"
+ reuse "github.com/libp2p/go-reuseport"
+ neticmp "golang.org/x/net/icmp"
+ netipv4 "golang.org/x/net/ipv4"
+ netipv6 "golang.org/x/net/ipv6"
+
+ "golang.zx2c4.com/wireguard/tun/netstack"
+ "gvisor.dev/gvisor/pkg/tcpip"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
+ "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
+ "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
+ "gvisor.dev/gvisor/pkg/tcpip/stack"
+ "gvisor.dev/gvisor/pkg/tcpip/transport/udp"
+
+ "wiretap/transport"
+)
+
+// udpConn holds socket addresses for source and destination.
+type udpConn struct {
+ Source netip.AddrPort
+ Dest netip.AddrPort
+}
+
+type dialerCount struct {
+ Count int
+ Port int
+}
+
+// source address -> new bind port
+var sourceMap = make(map[netip.AddrPort]dialerCount)
+var sourceMapLock = sync.RWMutex{}
+
+// source and destination -> dialer
+var connMap = make(map[udpConn](chan *stack.PacketBuffer))
+var connMapLock = sync.RWMutex{}
+
+// preroutingMatch matches packets in the prerouting stage.
+type preroutingMatch struct{}
+
+var s *stack.Stack
+
+// Match rejects all packets, but clones every prerouting packet to the packet handler.
+func (m preroutingMatch) Match(hook stack.Hook, packet *stack.PacketBuffer, inputInterfaceName, outputInterfaceName string) (matches bool, hotdrop bool) {
+ if hook == stack.Prerouting {
+ packetClone := packet.Clone()
+ go func() {
+ newPacket(packetClone)
+ packetClone.DecRef()
+ }()
+
+ // Taking control of packet, hotdrop.
+ return false, true
+ }
+
+ return false, false
+}
+
+func sourceMapLookup(n netip.AddrPort) (dialerCount, bool) {
+ sourceMapLock.RLock()
+ dc, ok := sourceMap[n]
+ sourceMapLock.RUnlock()
+
+ return dc, ok
+}
+
+func sourceMapIncrement(n netip.AddrPort, port int) {
+ sourceMapLock.Lock()
+ dc, ok := sourceMap[n]
+ if ok {
+ dc.Count += 1
+ sourceMap[n] = dc
+ } else {
+ sourceMap[n] = dialerCount{Count: 1, Port: port}
+ }
+ sourceMapLock.Unlock()
+}
+
+func sourceMapDecrement(n netip.AddrPort) {
+ sourceMapLock.Lock()
+ dc, ok := sourceMap[n]
+ if ok {
+ dc.Count -= 1
+ if dc.Count <= 0 {
+ delete(sourceMap, n)
+ } else {
+ sourceMap[n] = dc
+ }
+ }
+ sourceMapLock.Unlock()
+}
+
+func connMapWrite(c udpConn, pktChan chan *stack.PacketBuffer) {
+ connMapLock.Lock()
+ connMap[c] = pktChan
+ connMapLock.Unlock()
+}
+
+func connMapDelete(c udpConn) {
+ connMapLock.Lock()
+ delete(connMap, c)
+ connMapLock.Unlock()
+}
+
+func connMapLookup(c udpConn) (chan *stack.PacketBuffer, bool) {
+ connMapLock.RLock()
+ pktChan, ok := connMap[c]
+ connMapLock.RUnlock()
+
+ return pktChan, ok
+}
+
+func getDataFromPacket(packet *stack.PacketBuffer) []byte {
+ netHeader := packet.Network()
+ transHeader := header.UDP(netHeader.Payload())
+ return transHeader.Payload()
+}
+
+// NewPacket handles every new packet and sending it to the proper UDP dialer.
+func newPacket(packet *stack.PacketBuffer) {
+ netHeader := packet.Network()
+ transHeader := header.UDP(netHeader.Payload())
+
+ source := netip.MustParseAddrPort(net.JoinHostPort(netHeader.SourceAddress().String(), fmt.Sprint(transHeader.SourcePort())))
+ dest := netip.MustParseAddrPort(net.JoinHostPort(netHeader.DestinationAddress().String(), fmt.Sprint(transHeader.DestinationPort())))
+
+ log.Printf("(client %v) - Transport: UDP -> %v", source, dest)
+
+ var pktChan chan *stack.PacketBuffer
+ var ok bool
+
+ conn := udpConn{Source: source, Dest: dest}
+ pktChan, ok = connMapLookup(conn)
+ if ok {
+ // Dialer already exists, just forward packet.
+ pktChan <- packet.Clone()
+ return
+ }
+
+ // Dialer doesn't exist, check if source address has been seen before.
+ dc, ok := sourceMapLookup(source)
+ port := dc.Port
+ if !ok {
+ // Source address never seen, choose new ephemeral port.
+ port = 0
+ }
+
+ // New packet channel and dialer need to be created.
+ pktChan = make(chan *stack.PacketBuffer, 1)
+ connMapWrite(conn, pktChan)
+
+ go handleConn(conn, port)
+
+ pktChan <- packet.Clone()
+}
+
+// Handle creates a DNAT rule that forwards destination packets to a udp listener.
+// Once a connection is accepted, it gets handed off to handleConn().
+func Handle(tnet *netstack.Net, ipv4Addr netip.Addr, ipv6Addr netip.Addr, port uint16, lock *sync.Mutex) {
+ s = tnet.Stack()
+
+ // Create NATing firewall rule.
+ // iptables -t nat -A PREROUTING -p udp -j DNAT --to-destination :
+ headerFilter := stack.IPHeaderFilter{
+ Protocol: udp.ProtocolNumber,
+ CheckProtocol: true,
+ }
+
+ match := preroutingMatch{}
+
+ rule4 := stack.Rule{
+ Filter: headerFilter,
+ Matchers: []stack.Matcher{match},
+ Target: &stack.DNATTarget{
+ Addr: tcpip.Address(ipv4Addr.AsSlice()),
+ Port: port,
+ NetworkProtocol: ipv4.ProtocolNumber,
+ },
+ }
+
+ rule6 := stack.Rule{
+ Filter: headerFilter,
+ Matchers: []stack.Matcher{match},
+ Target: &stack.DNATTarget{
+ Addr: tcpip.Address(ipv6Addr.AsSlice()),
+ Port: port,
+ NetworkProtocol: ipv6.ProtocolNumber,
+ },
+ }
+
+ tid := stack.NATID
+ transport.PushRule(s, rule4, tid, false)
+ transport.PushRule(s, rule6, tid, true)
+ lock.Unlock()
+
+ // UDP listener is handled in the prerouting rule, we can return.
+ log.Println("Transport: UDP listener up")
+}
+
+// handleConn proxies traffic between a source and destination.
+func handleConn(conn udpConn, port int) {
+ defer func() {
+ connMapDelete(conn)
+ }()
+
+ var mostRecentPacket *stack.PacketBuffer
+
+ // New dialer from source to destination.
+ laddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", port))
+ if err != nil {
+ log.Println("Failed to parse laddr", err)
+ return
+ }
+ raddr, err := net.ResolveUDPAddr("udp", conn.Dest.String())
+ if err != nil {
+ log.Println("Failed to parse raddr", err)
+ return
+ }
+
+ // Reusing port so we can get the ICMP unreachable message back.
+ // Would like to use ListenUDP, but we don't get ICMP unreachable.
+ newConn, err := reuse.Dial("udp", laddr.String(), raddr.String())
+ if err != nil {
+ log.Println("Failed new UDP bind", err)
+ return
+ }
+ defer newConn.Close()
+
+ // No other dialer with same source address has a port set, so we get to be the first!
+ tmp_addr, _ := net.ResolveUDPAddr("udp", newConn.LocalAddr().String())
+ sourceMapIncrement(conn.Source, tmp_addr.Port)
+
+ defer func() {
+ sourceMapDecrement(conn.Source)
+ }()
+
+ err = newConn.SetDeadline(time.Now().Add(30 * time.Second))
+ if err != nil {
+ log.Println("Failed to set deadline", err)
+ }
+
+ // Sends packet from peer to destination.
+ go func() {
+ for {
+ pktChan, _ := connMapLookup(conn)
+ pkt := <-pktChan
+ // Exit if packet is nil, other goroutine wants us to close.
+ if pkt == nil {
+ return
+ }
+
+ // Update most recent packet for unreachable.
+ mostRecentPacket = pkt.Clone()
+ data := getDataFromPacket(pkt)
+
+ _, err := newConn.Write(data)
+ pkt.DecRef()
+ if err != nil {
+ log.Println("Error sending packet:", err)
+ newConn.Close()
+ return
+ }
+
+ // Reset timer, we got a packet.
+ err = newConn.SetDeadline(time.Now().Add(30 * time.Second))
+ if err != nil {
+ log.Println("Failed to set deadline:", err)
+ }
+ }
+ }()
+
+ // Return packet from destination to peer.
+ newBuf := make([]byte, 4096)
+ for {
+ n, err := newConn.Read(newBuf)
+ if err != nil {
+ // Failed to read from conn, if connection refused send unreachable to peer.
+ if oerr, ok := err.(*net.OpError); ok {
+ if syserr, ok := oerr.Err.(*os.SyscallError); ok {
+ if syserr.Err == syscall.ECONNREFUSED {
+ go sendUnreachable(mostRecentPacket)
+ }
+ }
+ }
+
+ // Force closing of goroutine by reinjecting buffer.
+ newConn.Close()
+ pktChan, ok := connMapLookup(conn)
+ if ok {
+ pktChan <- nil
+ }
+ return
+ }
+
+ // Reset timer, we got a packet.
+ err = newConn.SetDeadline(time.Now().Add(30 * time.Second))
+ if err != nil {
+ log.Println("Failed to set deadline:", err)
+ }
+
+ // Write packet back to peer.
+ sendResponse(conn, newBuf[:n])
+ }
+}
+
+// sendResponse builds a UDP packet to return to the peer.
+// TCP doesn't need this because the NATing works fine, but with UDP the OriginalDst function fails.
+func sendResponse(conn udpConn, data []byte) {
+ var err error
+ var ipv4Layer *layers.IPv4
+ var ipv6Layer *layers.IPv6
+
+ udpLayer := &layers.UDP{
+ SrcPort: layers.UDPPort(conn.Dest.Port()),
+ DstPort: layers.UDPPort(conn.Source.Port()),
+ }
+
+ isIpv6 := conn.Dest.Addr().Is6()
+ if isIpv6 {
+ ipv6Layer = &layers.IPv6{
+ Version: 6,
+ SrcIP: conn.Dest.Addr().AsSlice(),
+ DstIP: conn.Source.Addr().AsSlice(),
+ NextHeader: layers.IPProtocolUDP,
+ HopLimit: 64,
+ }
+ err = udpLayer.SetNetworkLayerForChecksum(ipv6Layer)
+ } else {
+ ipv4Layer = &layers.IPv4{
+ Version: 4,
+ //IHL: 5,
+ SrcIP: conn.Dest.Addr().AsSlice(),
+ DstIP: conn.Source.Addr().AsSlice(),
+ Protocol: layers.IPProtocolUDP,
+ TTL: 64,
+ }
+ err = udpLayer.SetNetworkLayerForChecksum(ipv4Layer)
+ }
+ if err != nil {
+ log.Println("Failed to marshal response:", err)
+ return
+ }
+
+ buf := gopacket.NewSerializeBuffer()
+ options := gopacket.SerializeOptions{
+ ComputeChecksums: true,
+ FixLengths: true,
+ }
+
+ proto := ipv4.ProtocolNumber
+ if isIpv6 {
+ proto = ipv6.ProtocolNumber
+ err = gopacket.SerializeLayers(buf, options,
+ ipv6Layer,
+ udpLayer,
+ gopacket.Payload(data),
+ )
+ } else {
+ err = gopacket.SerializeLayers(buf, options,
+ ipv4Layer,
+ udpLayer,
+ gopacket.Payload(data),
+ )
+ }
+
+ if err != nil {
+ log.Println("Failed to serialize layers:", err)
+ return
+ }
+
+ tcpipErr := transport.SendPacket(s, buf.Bytes(), &tcpip.FullAddress{NIC: 1, Addr: tcpip.Address(conn.Source.Addr().AsSlice())}, proto)
+ if tcpipErr != nil {
+ log.Println("Failed to write:", tcpipErr)
+ return
+ }
+}
+
+// sendUnreachable sends an ICMP Port Unreachable packet to peer as if from
+// the original destination of the packet.
+func sendUnreachable(packet *stack.PacketBuffer) {
+ var err error
+ var ipv4Layer *layers.IPv4
+ var ipv6Layer *layers.IPv6
+ var icmpLayer []byte
+
+ defer packet.DecRef()
+ netHeader := packet.Network()
+ transHeader := header.UDP(netHeader.Payload())
+ transHeader.SetChecksum(0)
+ transHeaderPayload := transHeader.Payload()
+
+ isIpv6 := netHeader.DestinationAddress().To4() == ""
+
+ if isIpv6 {
+ ipv6Layer = &layers.IPv6{}
+ ipv6Layer, err = transport.GetNetworkLayer[header.IPv6](netHeader, ipv6Layer)
+ if err != nil {
+ log.Println("Could not decode Network header:", err)
+ return
+ }
+ ipv6Layer = &layers.IPv6{
+ Version: 6,
+ SrcIP: ipv6Layer.DstIP,
+ DstIP: ipv6Layer.SrcIP,
+ NextHeader: layers.IPProtocolICMPv6,
+ HopLimit: 64,
+ }
+ ipv6Header, ok := netHeader.(header.IPv6)
+ if !ok {
+ log.Println("Could not type assert IPv6 Network Header")
+ return
+ }
+ icmpLayer, err = (&neticmp.Message{
+ Type: netipv6.ICMPTypeDestinationUnreachable,
+ Code: layers.ICMPv6CodePortUnreachable,
+ Body: &neticmp.DstUnreach{
+ Data: append(ipv6Header, transHeader[:len(transHeader)-len(transHeaderPayload)]...),
+ },
+ }).Marshal(nil)
+ ipv6Layer.Length = uint16(len(icmpLayer))
+ } else {
+ ipv4Layer = &layers.IPv4{}
+ ipv4Layer, err = transport.GetNetworkLayer[header.IPv4](netHeader, ipv4Layer)
+ if err != nil {
+ log.Println("Could not decode Network header:", err)
+ return
+ }
+ ipv4Layer = &layers.IPv4{
+ Version: 4,
+ IHL: 5,
+ SrcIP: ipv4Layer.DstIP,
+ DstIP: ipv4Layer.SrcIP,
+ Protocol: layers.IPProtocolICMPv4,
+ TTL: 64,
+ }
+ ipv4Header, ok := netHeader.(header.IPv4)
+ if !ok {
+ log.Println("Could not type assert IPv6 Network Header")
+ return
+ }
+ icmpLayer, err = (&neticmp.Message{
+ Type: netipv4.ICMPTypeDestinationUnreachable,
+ Code: layers.ICMPv4CodePort,
+ Body: &neticmp.DstUnreach{
+ Data: append(ipv4Header, transHeader[:len(transHeader)-len(transHeaderPayload)]...),
+ },
+ }).Marshal(nil)
+ ipv4Layer.Length = uint16((int(ipv4Layer.IHL) * 4) + len(icmpLayer))
+ }
+ if err != nil {
+ log.Println("Failed to marshal response:", err)
+ return
+ }
+
+ buf := gopacket.NewSerializeBuffer()
+ options := gopacket.SerializeOptions{
+ ComputeChecksums: true,
+ }
+
+ proto := ipv4.ProtocolNumber
+ if isIpv6 {
+ proto = ipv6.ProtocolNumber
+ err = gopacket.SerializeLayers(buf, options,
+ ipv6Layer,
+ )
+ } else {
+ err = gopacket.SerializeLayers(buf, options,
+ ipv4Layer,
+ )
+ }
+ if err != nil {
+ log.Println("Failed to serialize layers:", err)
+ return
+ }
+
+ response := append(buf.Bytes(), icmpLayer...)
+
+ tcpipErr := transport.SendPacket(s, response, &tcpip.FullAddress{NIC: 1, Addr: netHeader.SourceAddress()}, proto)
+ if tcpipErr != nil {
+ log.Println("Failed to write:", tcpipErr)
+ return
+ }
+}
diff --git a/wiretap.Dockerfile b/wiretap.Dockerfile
new file mode 100644
index 0000000..3be2bf2
--- /dev/null
+++ b/wiretap.Dockerfile
@@ -0,0 +1,20 @@
+FROM golang:1.19
+
+ARG http_proxy
+ARG https_proxy
+
+# Utilities for testing
+RUN apt-get update
+RUN apt-get install net-tools nmap dnsutils tcpdump iproute2 vim netcat iputils-ping wireguard iperf xsel -y
+
+WORKDIR /wiretap
+COPY ./src/go.mod ./src/go.sum ./
+RUN go mod download -x
+
+# Build Wiretap
+COPY ./src /wiretap
+
+RUN make BIN=.
+
+# Run webserver for testing
+CMD python3 -m http.server --bind :: 80