Merge branch 'v2' into develop

This commit is contained in:
ekzyis 2024-04-20 21:30:23 +02:00
commit db191dcac1
69 changed files with 80 additions and 3117 deletions

7
.gitignore vendored
View File

@ -1,4 +1,3 @@
html/pages/blog
public/**/*.html
hot-reload
renderer
# hugo
public/
.hugo_build.lock

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "themes/holy"]
path = themes/holy
url = git@github.com:ekzyis/holy.git

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 ekzyis
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, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

View File

@ -1,33 +0,0 @@
.PHONY: build render all dev
MARKDOWN=$(wildcard blog/*.md)
TARGETS= \
$(patsubst blog/%,public/blog/%,$(wildcard blog/*.html)) \
$(patsubst blog/%,public/blog/%,$(MARKDOWN:.md=.html)) \
$(patsubst html/pages/%,public/%,$(wildcard html/pages/*.html))
all: build render
build: renderer
dev:
bash sync-dev.sh
render: $(TARGETS)
renderer: *.go
go build -o renderer .
public/blog/index.html: blog/index.html $(MARKDOWN) renderer
mkdir -p public/blog html/pages/blog
./renderer -src $< > html/pages/$<
./renderer -src html/pages/$< > $@
public/blog/%.html: blog/%.md renderer
mkdir -p public/blog html/pages
./renderer -src $< > html/pages/$(<:.md=.html)
./renderer -src html/pages/$(<:.md=.html) > $@
public/%.html: html/pages/%.html renderer
mkdir -p public
./renderer -src $< > $@

39
WEB.md
View File

@ -1,39 +0,0 @@
# ekzyis.com
My personal website including blog.
## Development
This site consists of only static HTML, CSS, JS in public/.
The files are built (or "rendered") with the golang [`text/template`](https://pkg.go.dev/text/template) standard package. It doesn't use [`html/template`](https://pkg.go.dev/html/template) since I had problems including HTML like a common header, navigation menu, footer for a reusable layout. But this shouldn't be a problem since there is no user-generated content (yet?).
To build the files, a [Makefile](./Makefile) is used.
Run `make build` to create the `renderer` binary.
Run `make render` to render all files in public/.
Deployment is done by rendering all files in production mode and then copying them where a webserver like `nginx` can serve them.
I use [`deploy.sh`](./deploy.sh) for this.
## How to create new blog post
1. Create new Markdown file in blog/
2. It needs to have this header:
```
Title: title
Date: date
ReadingTime: time
Sats: 0
Comments: comments
---
```
3. Update `ComputeTitle` in `html.go` (TODO: make this no longer required)
4. Run `make render`.
Done!

5
archetypes/default.md Normal file
View File

@ -0,0 +1,5 @@
+++
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
date = {{ .Date }}
draft = true
+++

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

View File

@ -1,969 +0,0 @@
Title: Demystifying WireGuard and iptables
Date: 2023-08-09
ReadingTime: 15 minutes
Sats: 11623
Comments: https://stacker.news/items/221471
---
<details open>
<summary><b><a name="index">index</a></b></summary>
<div class="flex flex-column">
<a href="#introduction">0. introduction</a>
<a href="#iptables-primer">1. iptables primer</a>
<a href="#wireguard">2. wireguard</a>
<a class="subh" href="#wireguard-installation"><i>2.1 installation</i></a>
<a class="subh" href="#wireguard-configuration"><i>2.1 configuration</i></a>
<a class="subh" href="#wireguard-interface-control"><i>2.2 interface control</i></a>
<a href="#firewall">3. firewall configuration</a>
<a class="subh" href="#firewall-initial-configuration"><i>3.1 initial</i></a>
<a class="subh" href="#firewall-local-output-configuration"><i>3.1 local output</i></a>
<a class="subh" href="#firewall-remote-input-configuration"><i>3.2 remote input</i></a>
<a class="subh" href="#firewall-remote-output-configuration"><i>3.3 remote output</i></a>
<a class="subh" href="#firewall-local-input-configuration"><i>3.4 local input</i></a>
<a class="subh" href="#firewall-final-configuration"><i>3.5 final</i></a>
</div>
</details>
---
<a class="header" name="introduction">
# introduction
</a>
In this blog post, I will show you how to setup [WireGuard](https://wireguard.com) and
configure your Linux firewall with [`iptables`](https://wiki.archlinux.org/title/iptables).
We will establish a point-to-point connection between two machines across the internet.<br />
One machine ("the local machine") sits behind a [NAT](https://en.wikipedia.org/wiki/Network_address_translation) router and therefore is not reachable from the public internet.
The other machine ("the remote machine") has a static public IPv4 address and sits in a data center
(for example, if you rent a server from cloud providers).
This is the simplest network topology and therefore useful to get the basics down first.
In following blog articles, I will show you how to create more advanced network topologies
which include VPN gateways and port forwarding [[0]](#ft-0).
VPN gateways connect multiple devices together and port forwarding is usually used to
expose internal services to the internet.
It is important to me that you get a good understanding of `iptables`
and how to use it from this post since I think there is a lack of good guides about it.
I believe it is way more helpful to explain fundamentals well compared to just handing out instructions to follow.
With a good understanding, you will be able to help yourself a lot better in case you run into problems.
---
<a class="header" name="iptables-primer">
# iptables primer
</a>
`iptables` is a command line utility for configuring Linux kernel firewalls.
It acts upon _tables_ which consist of _chains_ which in turn consist of _rules_ and a _policy_.
The rules have criteria for packets and _targets_ like _ACCEPT_ or _DROP_.
These targets are basically actions which are executed if a packet matches.
For example, a rule could check if a packet comes from a specific IP address range and use the ACCEPT
target to accept it and let it pass through the firewall. Chain policies are used as fallback targets in case no
rule matches.
Each table has a specific purpose.
For example, the default table (which is used if you don't specify a table in a command) is the _filter_
table which contains an _INPUT_, _FORWARD_ and _OUTPUT_ chain and is used to accept or drop IP packets.
This is the only table we will need in this post. There is also the _nat_ table
(which we will use for port forwarding in a future blog post) and [three others](https://wiki.archlinux.org/title/iptables#Tables), but they are very specialized and thus not needed in the vast majority of use cases.
During the lifetime of a packet inside the Linux firewall,
the chains of tables are consulted to decide the fate of a packet.
The chains traverse through their rules in order until they find a matching rule whose target terminates the chain
[[1]](#ft-1). If no matching rule was found, then the chain's policy target is used.
The order of the chains is defined in this (simplified) flow chart [[2]](#ft-2):
<img class="flex m-auto" src="/blog/img/iptables_flowchart.png" alt="" />
Packets that come in on any network interface enter this flow chart at the top
and thus go first through the PREROUTING chain of the nat table.
However, not all packets originate from outside a network interface or reach the bottom of this flow chart.
Packets for local processes stop at <code class="bg-transparent">[local process]</code> and packets which are generated by local
processes enter this flow chart at <code class="bg-transparent">[local process]</code>.
The routing decision after the PREROUTING chain involves deciding if the final destination of the packet is the
local machine (in which case the packet traverses to the INPUT chain at the left)
or another machine in case we act as a router (in which case the packet traverses to the FORWARD chain at the right).<br />
The routing decision after <code class="bg-transparent">[local process]</code> involves assigning a source address, which outgoing
interface to use and other necessary information.
Since the packet may have changed inside the nat table which could affect routing,
there is a final routing decision just before the POSTROUTING chain to consider these changes.
With `iptables -S` (short for `iptables --list-rules`), we can lookup the current firewall configuration for a table.
You can specify a table like this: `iptables -t nat -S`.
For example, the filter table of a host inside a home network could look like this
_(as mentioned, if you don't specify a table, the filter table is used)_:
```
$ iptables -S
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -j LOG --log-prefix "[INPUT:DROP] " --log-level 6
-A OUTPUT -p udp -m udp --sport 67:68 --dport 67:68 -m comment --comment DHCP -j ACCEPT
-A OUTPUT -p udp -m udp --dport 53 -m comment --comment DNS -j ACCEPT
-A OUTPUT -p udp -m udp --sport 123 --dport 123 -m comment --comment NTP -j ACCEPT
-A OUTPUT -p tcp -m tcp --dport 80 -m comment --comment HTTP -j ACCEPT
-A OUTPUT -p tcp -m tcp --dport 443 -m comment --comment HTTPS -j ACCEPT
-A OUTPUT -p tcp -m tcp --dport 22 -m comment --comment SSH -j ACCEPT
-A OUTPUT -j LOG --log-prefix "[OUTPUT:DROP] " --log-level 6
```
The first three statements show that the chain policy for the INPUT, FORWARD and OUTPUT chains is to drop packets.
This means that any packet not matched by any (terminating) rule will be dropped.
The OUTPUT chain contains rules to allow outgoing packets for various common application layer protocols
like DHCP, DNS, NTP, HTTP, HTTPS and SSH.
The INPUT chain contains a rule which only allows packets for _related_ or _established_ connections.
This makes the firewall _stateful_.
Additionally, packets are logged before they are dropped by the chain policies. This uses the _LOG_ target
which is a non-terminating target. Logging packets is very useful for debugging
or can be used before applying new firewall rules to make sure you don't lock yourself out accidentally.
A packet is considered to be part of an established connection if it is part of a response to outgoing or incoming
packets. For example, if you're browsing a website, the incoming packets carrying the requested web page
content would be considered as part of an established connection. This makes the firewall stateful since it needs
to keep an internal state of outgoing and incoming packets.
A connection is considered related when it is related to another established connection.
A good example of related connections are data connections in FTP since FTP creates new connections for data
transfers instead of reusing the current established connection to the server. Kernel modules like
[this](https://github.com/torvalds/linux/blob/master/net/netfilter/nf_conntrack_ftp.c)
implement connection tracking for individual protocols such that related connections can be found by the firewall.
The syntax in the output of `iptables -S` is the same syntax you would use to configure the firewall.<br />
This means that we can set the INPUT chain policy to ACCEPT with
```
$ iptables -P INPUT ACCEPT
```
whereas with
```
$ iptables -A INPUT -p tcp --dport 3000 -j ACCEPT
```
we could append a rule to the INPUT chain to open port 3000 for TCP packets.
Other useful commands are
```
$ iptables -D <chain> <rulenum>
```
to delete rules or
```
$ iptables -R <chain> <rulenum> <rule-specification>
```
to replace rules with a rule number and a rule specification like
`-p <proto> --dport <destination port> -j <target>`.
You can look up rule numbers with `--line-numbers`:
```
$ iptables -S --line-numbers
```
These were all commands we will use in this blog post.
If anything is still unclear, don't hesitate to refer to the [manual](https://man.archlinux.org/man/iptables.8.en)
or ask a question in the [comments]({{- .Comments -}}).
---
<a class="header" name="wireguard">
# wireguard
</a>
<a class="header" name="wireguard-installation">
## installation
</a>
Follow the instructions [here](https://www.wireguard.com/install) to install WireGuard on your local and remote machine.<br />
When you are done, you should be able to run the following command:
```
$ wg --version
wireguard-tools v1.0.20210914 - https://git.zx2c4.com/wireguard-tools/
```
<a class="header" name="wireguard-configuration">
## configuration
</a>
WireGuard is a peer-to-peer (P2P) protocol like Bitcoin.
This means that by default, the protocol does not distinguish between servers and clients.
To create VPN gateways or any other network topology, you will have to configure your peers accordingly.
In this blog post, we will only connect two peers together so we can keep the configuration simple.
### key generation
WireGuard uses asymmetric cryptography for its encryption.
Therefore, you need to generate a key pair using the commands `genkey` and `pubkey`
on your local and remote machine.
As mentioned in [`man wg`](https://man.archlinux.org/man/wg.8#pubkey),
you can generate a key pair with secure file permissions (handled by [`umask`](https://wiki.archlinux.org/title/umask))
like this:
```
$ umask 077
$ wg genkey | tee /etc/wireguard/wg_private.key | wg pubkey > /etc/wireguard/wg_public.key
```
This will generate a private key at _/etc/wireguard/wg\_private.key_
and a public key at _/etc/wireguard/wg\_public.key_ which are only readable and writeable by the current user
[[3]](#ft-3).
_local machine keys:_
```
$ cat /etc/wireguard/wg_private.key
l0c4l+s3cR37+RDr+dJdgX/ACeRQLANiduQRJK9O23A=
$ cat /etc/wireguard/wg_public.key
/wH4OzafBUJVvRGzK8itUweV/GpwoUzn7OS99lr7gHI=
```
_remote machine keys:_
```
$ cat /etc/wireguard/wg_private.key
r3M073+s3cR37+fouaQZbP5QqfgwypHjKGBNmztxNEc=
$ cat /etc/wireguard/wg_public.key
GL33DRrI8/2yAT6+r5mTtBLd7CoErAAsio3yNqQ3K1M=
```
### ip range selection
You need to decide which IP range you want to use for your virtual private network (VPN).
This will be the IP range from which you will assign IP addresses to hosts inside the VPN.
**The important part is to not pick an IP range which is already in use.**
Fortunately, the IPv4 specification reserved following IP ranges for use in private networks [[4]](#ft-4):
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
IP addresses in these ranges are not routable in the public internet since they are ignored by all public routers.
For example, my local area network (LAN) uses 192.168.178.0/24 [[5]](#ft-5):
```
$ ip address
... other output ...
2: enp3s0: &ltBROADCAST,MULTICAST,UP,LOWER_UP&gt mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 9c:6b:00:06:a7:54 brd ff:ff:ff:ff:ff:ff
inet 192.168.178.146/24 brd 192.168.178.255 scope global dynamic noprefixroute enp3s0
valid_lft 804365sec preferred_lft 804365sec
inet6 fe80::6de5:ba8f:c52b:52bd/64 scope link noprefixroute
valid_lft forever preferred_lft forever
```
If you are already part of other private networks (company or university VPN for example),
you can check the IP ranges they use by connecting and running `ip address` afterwards as above.<br />
In this blog post, we will assume that **10.172.16.0/24** is still free to use and thus can be selected for our VPN.
### peer configuration
We can configure our peers via the file _/etc/wireguard/wg0.conf_ [[6]](#ft-6).
As with the keys, it makes sense to run `umask 077` before creating the files.
The files will then be created with read and write access only given to the current user.
For every peer, we need to define the interface by specifying the private key and IP address.
For the remote machine, we also need to set `ListenPort` to specify on which port the machine should
listen for incoming WireGuard UDP packets.
We don't set it for the local machine since we don't need a fixed port.
We only need a fixed port if peers need to know the port in advance to initiate connections.
However, the local machine is not reachable from the internet so it is not possible to initiate connections to it.
Therefore, we rely on the local machine initiating connections.
WireGuard will pick a random free port when the interface is brought up.
We also need to define the peers of every peer in the configuration.
This is done by adding a peer section which starts with <code class="bg-transparent">[Peer]</code> per peer.
Since we only have one peer per peer, there will only be a single <code class="bg-transparent">[Peer]</code> section per configuration.<br />
We need to set the public key of every peer such that WireGuard can use this public key to encrypt the packets.
The peers can then decrypt the packets using their private key. We also need to set which IP addresses we want to
route to each peer via <code class="bg-transparent">AllowedIPs</code>.
In our case here, this will just be the IP address of each peer. When we setup a VPN gateway,
this will be more interesting since there, we will route packets through multiple peers.
For the local machine, we will also set <code class="bg-transparent">Endpoint</code> to the **public** IP address of the remote machine
and port as used in <code class="bg-transparent">ListenPort</code>.
This lets WireGuard know how to reach the peer to establish a VPN connection.
To keep the connection alive, we will also use <code class="bg-transparent">PersistentKeepalive</code> in the local machine configuration.
This specifies the interval in seconds in which keep-alive packets are sent.
Without this, stateful firewalls may kill the VPN connection after some time since
WireGuard is not a chatty protocol by itself.
Additionally, our local machine is behind NAT which is another reason to use <code class="bg-transparent">PersistentKeepalive</code>
to keep NAT mappings valid.
_local machine configuration_:
```
[Interface]
Address = 10.172.16.2/32
PrivateKey = l0c4l+s3cR37+RDr+dJdgX/ACeRQLANiduQRJK9O23A=
[Peer]
AllowedIPs = 10.172.16.1/32
PublicKey = GL33DRrI8/2yAT6+r5mTtBLd7CoErAAsio3yNqQ3K1M=
Endpoint = 139.162.153.133:51913
PersistentKeepalive = 25
```
_remote machine configuration_:
```
[Interface]
Address = 10.172.16.1/32
PrivateKey = r3M073+s3cR37+fouaQZbP5QqfgwypHjKGBNmztxNEc=
ListenPort = 51913
[Peer]
AllowedIPs = 10.172.16.2/32
PublicKey = /wH4OzafBUJVvRGzK8itUweV/GpwoUzn7OS99lr7gHI=
```
As a last step, make sure that the file permissions are correctly set and the owner of all created files is root:
```
$ ls -l /etc/wireguard
-rw------- 1 root root 1155 Aug 03 15:38 wg0.conf
-rw------- 1 root root 45 Aug 03 15:31 wg_private.key
-rw-r--r-- 1 root root 45 Aug 03 15:31 wg_public.key
```
If the public key is readable by other users, that's fine.
If there is something wrong with your file permissions, run these commands:
```
$ chown root:root -R /etc/wireguard
$ chmod 600 -R /etc/wireguard
```
<a class="header" name="wireguard-interface-control">
## interface control
</a>
We are now done with all WireGuard configuration.
Run this on the local and remote machine to bring the interfaces up:
```
$ wg-quick up wg0
```
If you use `systemd`, you can run `wg-quick up wg0` on boot using a systemd service:
```
$ systemctl enable wg-quick@wg0
```
To take the interface down, run this:
```
$ wg-quick down wg0
```
To see the configuration and peer information of WireGuard interfaces, run `wg`:
```
$ wg
interface: wg0
public key: /wH4OzafBUJVvRGzK8itUweV/GpwoUzn7OS99lr7gHI=
private key: (hidden)
listening port: 60646
peer: GL33DRrI8/2yAT6+r5mTtBLd7CoErAAsio3yNqQ3K1M=
endpoint: 139.162.153.133:51913
allowed ips: 10.172.16.1/32
transfer: 0 B received, 444 B sent
persistent keepalive: every 25 seconds
```
As you can see in the line beginning with `transfer`, we did not receive any packets yet.
This is because we did not properly configure our firewalls yet.
---
<a class="header" name="firewall">
# firewall configuration with iptables
</a>
<a class="header" name="firewall-initial-configuration">
## initial configuration
</a>
We are starting with the following minimal set of firewall rules for the local machine:
```
(local) $ iptables -S
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
```
and remote machine:
```
(remote) $ iptables -S
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
```
You should be able to see that these firewall rules only allow SSH access from
the local machine to the remote machine with IP address 139.162.153.133.
Since WireGuard uses UDP, it therefore makes sense that we currently don't have a VPN
connection. We can check if we have a VPN connection with <code class="code">wg</code>
(check for latest handshake or received bytes)
or by trying to ping one machine from the other. Therefore, we run
```
(local) $ ping 10.172.16.1
```
at the local machine and
```
(remote) $ ping 10.172.16.2
```
at the remote machine.
We need to run both commands since it is not guaranteed that the other direction also works if one machine can
reach the other as you will later see. We will also keep these commands running until the end so we can
immediately see if a VPN connection is up or was lost.
To fully understand which rules are required and why, we will configure the firewall in four steps:
1. Configure local OUTPUT chain with remote INPUT chain policy set to ACCEPT
2. Keep VPN connection up with remote INPUT chain policy switched back to DROP
3. Configure remote OUTPUT chain with local INPUT chain policy set to ACCEPT
4. Keep VPN connection up with local INPUT chain policy switched back to DROP
By first setting the INPUT chain policy to ACCEPT in the receiving machine,
we can focus on a single machine at every step since we know
that only the OUTPUT rules can currently be responsible for any connection failure.
<a class="header" name="firewall-local-output-configuration">
## local OUTPUT chain configuration
</a>
As mentioned, we will set the INPUT chain policy of the remote filter table to ACCEPT first:
```
(remote) $ iptables -P INPUT ACCEPT
```
```diff
- -P INPUT DROP
+ -P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
```
We know that `ping` uses ICMP packets so we need to allow ICMP in our local firewall:
```
(local) $ iptables -A OUTPUT -p icmp -j ACCEPT
```
We also know that WireGuard uses UDP. This mean we need to also allow outgoing UDP packets:
```
(local) $ iptables -A OUTPUT -p udp -j ACCEPT
```
We have made these changes locally now:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
+ -A OUTPUT -p icmp -j ACCEPT
+ -A OUTPUT -p udp -j ACCEPT
```
This is sufficient for a ping from the local to the remote machine:
```
(local) $ ping 10.172.16.1
PING 10.172.16.1 (10.172.16.1) 56(84) bytes of data.
64 bytes from 10.172.16.1: icmp_seq=18 ttl=64 time=9.28 ms
64 bytes from 10.172.16.1: icmp_seq=19 ttl=64 time=8.88 ms
64 bytes from 10.172.16.1: icmp_seq=20 ttl=64 time=9.25 ms
```
The current rules are very broad however. This is bad for security. We will fix this now.
However, if we limit UDP packets to only the `wg0` interface, the ping stops working:
```
(local) $ iptables -R OUTPUT 3 -o wg0 -p udp -j ACCEPT
```
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -p icmp -j ACCEPT
- -A OUTPUT -p udp -j ACCEPT
+ -A OUTPUT -o wg0 -p udp -j ACCEPT
```
This is because <code class="bg-transparent">wg0</code> is the _virtual network interface_, not the actual physical network
interface that sends the UDP packets. WireGuard works by wrapping all packets (like ICMP here) in UDP packets
before sending them out "over the wire". The following chart should make more clear what this means:
![](/blog/img/wireguard_layering.png)
If we use the physical network interface
(which is `enp3s0` for the local machine as can be seen in `ip address`), the ping works again:
```
(local) $ iptables -R OUTPUT 3 -o enp3s0 -p udp -j ACCEPT
```
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -p icmp -j ACCEPT
- -A OUTPUT -o wg0 -p udp -j ACCEPT
+ -A OUTPUT -o enp3s0 -p udp -j ACCEPT
```
We can also limit the UDP packets to port 51913 of our remote machine:
```
(local) $ iptables -R OUTPUT 3 -o enp3s0 -p udp -d 139.162.153.133 --dport 51913 -j ACCEPT
```
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -p icmp -j ACCEPT
- -A OUTPUT -o enp3s0 -p udp -j ACCEPT
+ -A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
What would not work is to limit the UDP packets using internal IPs
since the physical network interface is unaware of our VPN:
```
(local) $ iptables -R OUTPUT 3 -o enp3s0 -p udp -d 10.172.16.1 -j ACCEPT
```
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -p icmp -j ACCEPT
- -A OUTPUT -o enp3s0 -p udp -j ACCEPT
+ -A OUTPUT -d 10.172.16.1/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
To confirm our understanding, we can limit the ICMP packets to only the `wg0` interface.
The ping should continue to work:
```
(local) $ iptables -R OUTPUT 2 -o wg0 -p icmp -j ACCEPT
```
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
- -A OUTPUT -p icmp -j ACCEPT
+ -A OUTPUT -o wg0 -p icmp -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
And it indeed does:
```
64 bytes from 10.172.16.1: icmp_seq=50 ttl=64 time=9.05 ms
64 bytes from 10.172.16.1: icmp_seq=51 ttl=64 time=8.78 ms
64 bytes from 10.172.16.1: icmp_seq=52 ttl=64 time=9.12 ms
```
Usually, all traffic is allowed inside a VPN. Therefore, this rule is commonly used:
```
(local) $ iptables -R OUTPUT 2 -o wg0 -j ACCEPT
```
Done. The changes we applied to the local firewall configuration are:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
+ -A OUTPUT -o wg0 -j ACCEPT
+ -A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
<a class="header" name="firewall-remote-input-configuration">
## switch remote INPUT chain policy back to DROP
</a>
We will set the remote INPUT chain policy back to DROP now.
```diff
- -P INPUT ACCEPT
+ -P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
```
The ping stopped working but we know that the local OUTPUT chain is properly configured.
After allowing inbound ICMP _and_ UDP packets, the ping from the local machine to the remote machine works again:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
+ -A INPUT -p icmp -j ACCEPT
+ -A INPUT -p udp -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
```
We will limit the UDP packets to only port 51913 and the physical network interface.
The physical network interface of the remote machine is `eth0`:
```
(remote) $ iptables -R INPUT 3 -i eth0 -p udp --dport 51913 -j ACCEPT
```
To actually enable all VPN traffic from the local to the remote machine,
we also need to allow it on the remote machine:
```
(remote) $ iptables -R INPUT 2 -i wg0 -j ACCEPT
```
Done. We applied the following changes to the remote firewall:
```diff
- -P INPUT ACCEPT
+ -P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
+ -A INPUT -i wg0 -j ACCEPT
+ -A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
```
<a class="header" name="firewall-remote-output-configuration">
## remote OUTPUT chain configuration
</a>
We will take care of pinging the local machine from the remote machine now. As you can see, having a connection
from one direction does not mean that the other direction works, too (even though response packets arrive).
To focus on the OUTPUT chain configuration, we will set the local INPUT chain policy to ACCEPT:
```diff
- -P INPUT DROP
+ -P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
And start with allowing outgoing ICMP packets:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
+ -A OUTPUT -p icmp -j ACCEPT
```
However, this time, we notice that the ping already works even without allowing UDP packets:
```
(remote) $ ping 10.172.16.2
PING 10.172.16.2 (10.172.16.2) 56(84) bytes of data.
64 bytes from 10.172.16.2: icmp_seq=8 ttl=64 time=9.16 ms
64 bytes from 10.172.16.2: icmp_seq=9 ttl=64 time=8.74 ms
64 bytes from 10.172.16.2: icmp_seq=10 ttl=64 time=8.95 ms
```
The explanation is that the UDP packets are able to use an established connection.
Only allowing TCP packets kills the connection [[7]](#ft-7):
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
- -A OUTPUT -m state --state ESTABLISHED -j ACCEPT
+ -A OUTPUT -p tcp -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -p icmp -j ACCEPT
```
We could allow UDP packets through a separate rule ... :
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
-A OUTPUT -p tcp -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -p icmp -j ACCEPT
+ -A OUTPUT -p udp -j ACCEPT
```
... but since we don't have a specific IP address for limiting the traffic,
we will revert back to the previous stateful rule and also allow any traffic from the virtual network interface:
```
(remote) $ iptables -R OUTPUT 1 -m state --state ESTABLISHED -j ACCEPT
(remote) $ iptables -D OUTPUT 3
(remote) $ iptables -R OUTPUT 2 -o wg0 -j ACCEPT
```
Done. We effectively only added a single rule to the remote firewall:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
+ -A OUTPUT -o wg0 -j ACCEPT
```
<a class="header" name="firewall-local-input-configuration">
## switch local INPUT policy back to DROP
</a>
We have a bidirectional connection now. The only thing left to do is to revert back to
a local INPUT chain policy of DROP and keep the connection up.
When going back to DROP as the INPUT chain policy ... :
```diff
- -P INPUT ACCEPT
+ -P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
... we notice that the ping continues to work. This is because of the first INPUT rule:
```
-A INPUT -m state --state ESTABLISHED -j ACCEPT
```
If we kill the connection and then run `ping` again, it no longer works:
```
64 bytes from 10.172.16.2: icmp_seq=93 ttl=64 time=9.16 ms
64 bytes from 10.172.16.2: icmp_seq=94 ttl=64 time=9.06 ms
64 bytes from 10.172.16.2: icmp_seq=95 ttl=64 time=8.83 ms
64 bytes from 10.172.16.2: icmp_seq=96 ttl=64 time=9.15 ms
^C
--- 10.172.16.2 ping statistics ---
96 packets transmitted, 96 received, 0% packet loss, time 95139ms
rtt min/avg/max/mdev = 8.602/9.114/9.606/0.209 ms
(remote) $ ping 10.172.16.2
PING 10.172.16.2 (10.172.16.2) 56(84) bytes of data.
```
This is expected since it only worked because of an established connection.
After allowing ICMP packets, the ping also works immediately again:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
+ -A INPUT -p icmp -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
This is again because the UDP packets can still use the established VPN connection.
This is similar to what happened while configuring the remote OUTPUT chain.
But again, the proper configuration would be to allow all traffic into the <code>wg0</code> interface
but limit incoming UDP packets with a source IP address and port filter:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
- -A INPUT -p icmp -j ACCEPT
+ -A INPUT -i wg0 -j ACCEPT
+ -A INPUT -s 139.162.153.133/32 -i enp3s0 -p udp -m udp --sport 51913 -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
<a class="header" name="firewall-final-configuration">
## final configuration
</a>
_local firewall configuration:_
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
+ -A INPUT -i wg0 -j ACCEPT
+ -A INPUT -s 139.162.153.133 -i enp3s0 -p udp -m udp --sport 51913 -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
+ -A OUTPUT -o wg0 -j ACCEPT
+ -A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
_remote firewall configuration:_
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
+ -A INPUT -i wg0 -j ACCEPT
+ -A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
+ -A OUTPUT -o wg0 -j ACCEPT
```
---
Thanks for reading my first blog post!
If you want to read more content like this, please consider subscribing via [RSS](/blog/rss.xml).
Also, I would highly appreciate any feedback in the [comments]({{- .Comments -}}).
You can tell me if it was too long, too boring, too complicated or anything else, that's no problem!
I am very new to this whole blogging thing and thus could really _really_ need **any** kind of feedback.
I'll even pay you 100 sats!
---
<small>
<span id="ft-0">[0]
Originally, I wanted to make a blog post how to use WireGuard and port forwarding to expose your bitcoin
node at home to the internet with a static public IPv4 address. This avoids that inbound connections drop when
your ISP changes your public IPv4 address. However, I realized that I want to be thorough with explaining the
basics first and not skip anything just to get to the port forwarding part faster.
</span><br />
<span id="ft-1">[1]
Some targets don't terminate the chain. For example, targets can redirect to another user-defined chain and
then
return or just log a packet.
</span><br />
<span id="ft-2">[2]
This chart only contains the filter and nat table and was taken from
[here](https://wiki.archlinux.org/title/iptables#Basic_concepts).
</span><br />
<span id="ft-3">[3]
If you are confused by the mask 077 like me since it looks like it gives everyone full access except to
yourself:
as mentioned [here](https://wiki.archlinux.org/title/umask).
<code class="code">umask</code> uses
the logical complement of the permission bits. This means that any bit set via umask will
<strong>not</strong>
be set in the file permissions.
</span><br />
<span id="ft-4">[4] See the
[wikipedia article](https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses)
about private networks.
</span><br />
<span id="ft-5">[5]
If you are confused by the /24 notation, you can read about CIDR
[here](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing).
</span><br />
<span id="ft-6">[6] You could use any other path, but _/etc/wireguard/_ is searched automatically by
`wg-quick` so our commands can be kept short.
</span><br />
<span id="ft-7">[7]
I am not entirely sure which established connection the UDP packets use but I think it's the VPN connection
since it always uses the same network sockets as far as I know. If you know more about this, please let me
know!
</span><br />
</small>

View File

@ -1,386 +0,0 @@
Title: WireGuard Packet Forwarding
Date: 2023-08-21
ReadingTime: 10 minutes
Sats: 2593
Comments: https://stacker.news/items/230179
---
<details open>
<summary><b><a name="index">index</a></b></summary>
<div class="flex flex-column">
<a href="#introduction">0. introduction</a>
<a href="#initial-configuration">1. initial configuration</a>
<a href="#end-device-configuration">2. end device configuration</a>
<a href="#router-configuration">3. router configuration</a>
<a class="subh" href="#router-firewall-configuration"><i>3.1 firewall</i></a>
<a class="subh" href="#router-kernel-params"><i>3.1 kernel parameters</i></a>
<a href="#tcpdump">4. inspecting network traffic with tcpdump</a>
</div>
</details>
---
<a class="header" name="introduction">
# introduction
</a>
In my [previous blog post](/blog/20230809-demystifying-wireguard-and-iptables),
I have shown you how to setup your own VPN with [WireGuard](https://wireguard.com/) and [`iptables`](https://wiki.archlinux.org/title/iptables).
We have established a point-to-point connection between two peers where one peer (10.172.16.1) was reachable from the internet:
![](/blog/img/point-to-point.png)
Today, I will explain how more peers can be added to our VPN.
One peer ("the router") will be configured to forward packets between all other peers ("the end devices").
Therefore, our VPN will become a star network:
![](/blog/img/star_network.png)
You could then [install](https://www.wireguard.com/install/#android-play-store-direct-apk-file) WireGuard on your mobile device
and reach all other machines in your VPN from anywhere with internet connection.
To get a deeper understanding how this forwarding works, we will take a brief look at the network traffic with `tcpdump` after we configured our network.
---
<a class="header" name="initial-configuration">
# initial configuration
</a>
The end devices will start with a point-to-point connection to the router with 10.172.16.1 as its internal IP address.
The WireGuard and firewall configuration for these could therefore look like this:
**_WireGuard configuration for end device:_**
```
[Interface]
Address = 10.172.16.x/32
PrivateKey = <PRIVATE KEY OF PEER>
[Peer]
AllowedIPs = 10.172.16.1/32
PublicKey = <PUBLIC KEY OF ROUTER>
Endpoint = 139.162.153.133:51913
PersistentKeepalive = 25
```
**_firewall configuration for end device:_**
```
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -s 139.162.153.133 -i enp3s0 -p udp -m udp --sport 51913 -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
-A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
Since the router is connected to multiple peers, it will have multiple <code class="bg-transparent">[Peer]</code> sections in its configuration:
**_WireGuard configuration for router:_**
```
[Interface]
Address = 10.172.16.1/32
PrivateKey = <PRIVATE KEY OF ROUTER>
ListenPort = 51913
[Peer]
AllowedIPs = 10.172.16.2/32
PublicKey = <PUBLIC KEY OF PEER>
[Peer]
AllowedIPs = 10.172.16.4/32
PublicKey = <PUBLIC KEY OF PEER>
...
[Peer]
AllowedIPs = 10.172.16.25/32
PublicKey = <PUBLIC KEY OF PEER>
```
**_firewall configuration for router:_**
```
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
```
<small>_If these configurations are confusing to you, read my [previous blog post](/blog/20230809-demystifying-wireguard-and-iptables)._</small>
---
<a class="header" name="end-device-configuration">
# end device configuration
</a>
The only change we have to do on the end devices is to route all IP addresses within the VPN to the router peer.
We configure this in the WireGuard configuration file at _/etc/wireguard/wg0.conf_:
```diff
[Peer]
- AllowedIPs = 10.172.16.1/32
+ AllowedIPs = 10.172.16.0/24
PublicKey = &ltPUBLIC KEY OF ROUTER&gt
Endpoint = 139.162.153.133:51913
PersistentKeepalive = 25
```
To apply these changes, we run this command <span id="ft-0b">[[0]](#ft-0)</span>:
```
$ wg syncconf wg0 <(wg-quick strip wg0)
```
---
<a class="header" name="router-configuration">
# router configuration
</a>
<a class="header" name="router-firewall-configuration">
## firewall
</a>
The router requires no additional WireGuard configuration.
However, the router firewall needs to allow forwarding packets inside the VPN:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
+ -A FORWARD -i wg0 -o wg0 -j ACCEPT
```
<a class="header" name="router-kernel-params">
## kernel parameters
</a>
We also need to allow IP forwarding in the kernel. We can configure kernel parameters with `sysctl`.
To see the current value for IP forwarding:
```
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
```
This basically does the same as checking the content of the file _/proc/sys/net/ipv4/ip\_forward_:
```
$ cat /proc/sys/net/ipv4/ip_forward
0
```
To change the setting, we can use `sysctl -w`:
```
$ sysctl -w net.ipv4.ip_forward=1
```
However, this change is not persistent. To make sure the new setting is kept after reboot, we need to modify _/etc/sysctl.conf_.
We can do this by appending <code class="bg-transparent">net.ipv4.ip_forward = 1</code> to the file:
```
$ echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
```
To reload settings:
```
$ sysctl -p
```
---
<a class="header" name="tcpdump">
# inspecting the network traffic with tcpdump
</a>
We now should have a connection between every peer via the router.
To confirm this, we ping one machine (10.172.16.25) from another machine (10.172.16.6):
```
$ ping 10.172.16.25
PING 10.172.16.25 (10.172.16.25) 56(84) bytes of data.
64 bytes from 10.172.16.25: icmp_seq=1 ttl=63 time=138 ms
64 bytes from 10.172.16.25: icmp_seq=2 ttl=63 time=165 ms
64 bytes from 10.172.16.25: icmp_seq=3 ttl=63 time=182 ms
64 bytes from 10.172.16.25: icmp_seq=4 ttl=63 time=206 ms
64 bytes from 10.172.16.25: icmp_seq=5 ttl=63 time=229 ms
```
We will inspect this traffic on 10.172.16.1 (the router) and 10.172.16.6 (the pinging machine) with `tcpdump` now <span id="ft-1b">[[1]](#ft-1)</span>.
On the pinging machine, we will get the following output for the virtual network interface <span id="ft-2b">[[2]](#ft-2)</span>:
```
$ tcpdump -tni wg0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wg0, link-type RAW (Raw IP), snapshot length 262144 bytes
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 1, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 1, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 2, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 2, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 3, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 3, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 4, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 4, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 19, seq 5, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 19, seq 5, length 64
```
and this for the physical network interface (filtered by UDP packets from/to port 51913):
```
$ tcpdump -tni enp3s0 'udp and port 51913'
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enp3s0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
IP 192.168.178.146.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 192.168.178.146.51941: UDP, length 128
```
On the router, we get this output for the virtual network interface:
```
$ tcpdump -tni wg0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wg0, link-type RAW (Raw IP), snapshot length 262144 bytes
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 1, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 1, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 1, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 1, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 2, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 2, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 2, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 2, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 3, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 3, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 3, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 3, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 4, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 4, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 4, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 4, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 5, length 64
IP 10.172.16.6 > 10.172.16.25: ICMP echo request, id 21, seq 5, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 5, length 64
IP 10.172.16.25 > 10.172.16.6: ICMP echo reply, id 21, seq 5, length 64
```
and this for the physical network interface _(public IP addresses of end devices redacted)_:
```
$ tcpdump -tni eth0 'udp and port 51913'
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
IP 87.161.X.X.51941 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 109.43.X.X.11007: UDP, length 128
IP 109.43.X.X.11007 > 139.144.78.247.51913: UDP, length 128
IP 139.144.78.247.51913 > 87.161.X.X.51941: UDP, length 128
```
As you can see, for every ICMP echo request, we get an ICMP echo reply from 10.172.16.25 in the `tcpdump` output of 10.172.16.6.
We also see that we receive a response for every UDP packet sent to port 51913 of 139.144.78.247.
However, in the `tcpdump` output of 10.172.16.1 for <code class="bg-transparent">wg0</code>,
we see every packet twice. Why is that?
The reason for this is that `tcpdump` captures every incoming and outgoing packet <span id="ft-3b">[[3]](#ft-3)</span>.
Since we are forwarding packets on the virtual network interface, the packet for 10.172.16.25 from 10.172.16.6 will be captured on 10.172.16.1
when it enters the interface from 10.172.16.6 and when it leaves to 10.172.16.25.
The same happens when the response from 10.172.16.25 arrives at 10.172.16.1 and is forwarded to 10.172.16.6.
We can see this forwarding happening better in the `tcpdump` output for the physical network interface. There, we see that for every ICMP echo request, we see one UDP
packet from 87.161.X.X to 139.144.78.247 and then another from 139.144.78.247 to 109.43.X.X.
As you also can see, the forwarding does not change internal IP addresses. The packets still contain the same source and destination IP addresses.
This works because we have configured the interfaces of all peers to route all IP addresses towards the router peer.
So when 10.172.16.25 sees the ICMP echo request from 10.172.16.6 via 10.172.16.1, it will just respond to 10.172.16.6 again via 10.172.16.1.
The UDP packet IP addresses do seem changed, though. However, this is not network address translation (NAT) but just packet encapsulation.
As mentioned in my [previous blog post](/blog/20230809-demystifying-wireguard-and-iptables), the physical network interfaces have no notion of internal IP addresses
thus every UDP packet is sent using publicly routable IP addresses over the wire.
This means we are not doing any network address translation here hence the name forwarding.
Another important detail is that we do not need the following FORWARD rules in the router:
```
-A FORWARD -s 10.172.16.0/24 -i wg0 -o eth0 -j ACCEPT
-A FORWARD -d 10.172.16.0/24 -i eth0 -o wg0 -j ACCEPT
```
These rules are only required if we want to forward VPN traffic to <code class="bg-transparent">eth0</code> which faces the public internet.
Therefore, these rules would be part of a configuration to give internet access via 10.172.16.1 to peers inside the VPN.
---
Thanks for reading my second blog post! If you want to read more content like this, please consider subscribing via [RSS](https://dev.ekzyis.com/blog/rss.xml).
In the next blog post, we will use network address translation with the SNAT and DNAT targets in `iptables` for port forwarding.
This will make it possible to expose an internal service (like an HTTP server for example) to the public internet.
---
<small>
<span id="ft-0">[[0]](#ft-0b) `wg-quick strip wg0` returns the config in the format that `wg` can parse.
This is necessary because `wg-quick` <q>adds a few extra configuration values to the format understood by `wg` in order to
configure additional attributes of an interface</q>. ([source](https://man.archlinux.org/man/wg-quick.8.en#CONFIGURATION))
</span><br />
<span id="ft-1">[[1]](#ft-1b) Since 10.172.16.25 is a mobile device, I didn't include `tcpdump` output from that device. It also wouldn't include anything interesting.
</span><br />
<span id="ft-2">[[2]](#ft-2b) I used `-t` to not include timestamps and `-n` to not resolve IP addresses to hostnames. `-i <interface>` selects the interface we want to tap.
</span><br />
<span id="ft-3">[[3]](#ft-3b) To be precise, `tcpdump` captures incoming packets _before_ firewall processing
while outgoing packets will be captured _after_ firewall processing. This means `tcpdump` will capture incoming packets that will be dropped by the firewall
whereas outgoing packets that were dropped will not show up.
See [here](https://wiki.archlinux.org/title/Network_Debugging#Tcpdump) and [here](https://superuser.com/a/925332) for more information.
</span>
</small>

View File

@ -1,314 +0,0 @@
Title: WireGuard Port Forwarding
Date: 2023-09-25
ReadingTime: 10 minutes
Sats: 1803
Comments: https://stacker.news/items/265524
---
<details open>
<summary><b><a name="index">index</a></b></summary>
<div class="flex flex-column">
<a href="#introduction">0. introduction</a>
<a href="#nat-primer">1. NAT primer</a>
<a href="#port-forwarding">2. port forwarding</a>
<a class="subh" href="#port-forwarding-initial"><i>2.1 initial configuration</i></a>
<a class="subh" href="#port-forwarding-internal"><i>2.2 HTTP server on 10.172.16.2 <> 10.172.16.1</i></a>
<a class="subh" href="#port-forwarding-internet"><i>2.3 HTTP server on 10.172.16.2 <> 93.184.216.34</i></a>
<a class="subh" href="#port-forwarding-final"><i>2.4 final configuration</i></a>
</div>
</details>
---
<a class="header" name="introduction">
# introduction
</a>
Today, we will implement port forwarding with [`iptables`](https://wiki.archlinux.org/title/iptables). We will do this to expose a service to the public internet which is running inside a VPN.
The exposed service will be a basic HTTP server running on a host in a home network. This means that the host itself already has a connection to the public internet (as most hosts in a home network do) and is not only connected to all other hosts inside the VPN via a VPN router.
However, since the HTTP server is running inside the VPN, we will need to use port forwarding inside our VPN instead of on the router with which we access the public internet. Port forwarding on that home router won't work since it can't access our VPN service (even though the host it's running on is inside the home network!).
Essentially, we will update the network topology from the [previous blog post](/blog/20230821-wireguard-packet-forwarding) with a connection to the public internet:
![](/blog/img/star_network_with_internet_access.png)
As always, after reading my blog post, you will not only be able to setup your own HTTP server inside a VPN; but will actually understand what happens at the network level. With this knowledge, you will be able to adapt and find the best solution for your specific needs.
Therefore, we start with a primer about NAT which stands for _Network Address Translation_.
---
<a class="header" name="nat-primer">
# NAT primer
</a>
Network Address Translation was developed to mitigate the issue of not having enough IPv4 addresses for every device which wants to connect to the internet. IPv4 addresses are only 32 bits long which means there are only 2<sup>32</sup> = 4,294,967,296 addresses available. This may sound like a lot but when considering that more and more devices are connected to the internet (which isn't going to stop anytime soon) and that the world population is around 8 billion people, there simply aren't enough IPv4 addresses to go around. Hence a solution needed to be found.
With NAT, one can hide multiple devices behind a router and thus behind a single IPv4 address. The hosts connected to the internet via this router are assigned private IPv4 addresses and thus form a private network. Private IPv4 addresses are addresses which are not routable on the public internet. Routers on the public internet would simply not forward packets containing these IP addresses. The following IPv4 address ranges were reserved for such private use by [IANA](https://www.iana.org/) in [RFC1918](https://datatracker.ietf.org/doc/html/rfc1918#section-3):
- 10.0.0.8/8
- 172.16.0.0/12
- 192.168.0.0/16
This mitigates the problem since these private IP addresses only have to be unique per private network and not globally. That's also the reason why these addresses are not routable. How should a router know how to route these packets? There isn't a unique device with this IP address!
To enable internet access for hosts with only a private IPv4 address, every packet with a destination not inside the private network gets forwarded to the _NAT gateway_. Usually, this is the router. The router then replaces the private source IP address with its own public IP address <span id="ft-0b">[[0]](#ft-0)</span> and the port with another random port:
![](/blog/img/nat_send.png)
The NAT gateway then stores this replacement in a NAT table:
![](/blog/img/nat_table.png)
For arriving packets, this table is consulted to reverse the translation:
![](/blog/img/nat_recv.png)
The NAT IP address and NAT port are required to reverse this process.
Without them, we would not be able to distinguish multiple connections from the private network to the same destination IP address.
This method to allow multiple hosts inside a private network access to the internet is called _Source NAT_ (SNAT) since we change the source IP address when initiating the connection.
If we want to allow hosts from the public internet to access hosts in a private network, we use _Destination NAT_ (DNAT) since we change the destination IP address when the connection is initiated.
The reversal in both methods is automatically handled using the NAT table.
---
<a class="header" name="port-forwarding">
# Port forwarding
</a>
We will now apply this knowledge to expose an HTTP server running inside a VPN. Our setup will work like this:
![](/blog/img/nat_http.png)
We will have to use DNAT _and_ SNAT since we need to change the destination IP address to 10.172.16.2 _and_ the source IP address to 10.172.16.1 since we can only route internal IP addresses (10.172.16.0/24) over the virtual network interface to the HTTP server.
<a class="header" name="port-forwarding-initial">
## initial configuration
</a>
We will start with the following configuration of the VPN router which will also act as a NAT gateway:
_/etc/wireguard/wg0.conf @ 10.172.16.1:_
```
[Interface]
Address = 10.172.16.1/32
PrivateKey = r3M073+s3cR37+fouaQZbP5QqfgwypHjKGBNmztxNEc=
ListenPort = 51913
[Peer]
AllowedIPs = 10.172.16.2/32
PublicKey = /wH4OzafBUJVvRGzK8itUweV/GpwoUzn7OS99lr7gHI=
```
_firewall configuration @ 10.172.16.1:_
```
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
-A FORWARD -i wg0 -o wg0 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
```
IP forwarding is also enabled in the kernel:
```
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
```
The host on which we will run the HTTP server is configured like this:
_/etc/wireguard/wg0.conf @ 10.172.16.2:_
```
[Interface]
Address = 10.172.16.2/32
PrivateKey = l0c4l+s3cR37+RDr+dJdgX/ACeRQLANiduQRJK9O23A=
[Peer]
AllowedIPs = 10.172.16.0/24
PublicKey = GL33DRrI8/2yAT6+r5mTtBLd7CoErAAsio3yNqQ3K1M=
Endpoint = 93.184.216.34:51913
PersistentKeepalive = 25
```
_firewall configuration @ 10.172.16.2:_
```
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -s 93.184.216.34 -i enp3s0 -p udp -m udp --sport 51913 -j ACCEPT
-A OUTPUT -d 93.184.216.34/32 -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
-A OUTPUT -d 93.184.216.34/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT
```
<a class="header" name="port-forwarding-internal">
## HTTP server on 10.172.16.2 <> 10.172.16.1
</a>
With the existing configuration, we can run an HTTP server on 10.172.16.2 and access it from 10.172.16.1.
To keep it simple, we will use the built-in HTTP server from Python:
_10.172.16.2:_
```
$ python -m http.server -b 10.172.16.2 8000
```
As mentioned, we can already access the HTTP server from 10.172.16.1 with the existing configuration:
_10.172.16.1:_
```
$ curl -I 10.172.16.2:8000
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.11.5
Date: Sun, 24 Sep 2023 22:28:14 GMT
Content-type: text/html; charset=utf-8
Content-Length: 187
```
<a class="header" name="port-forwarding-internet">
## HTTP server on 10.172.16.2 <> 93.184.216.34
</a>
Our goal is to access the HTTP server using the public IP address of the VPN server.
With the following command, we try exactly this. We try to access the HTTP server running on the same host but inside the VPN over the public internet:
_10.172.16.2:_
```
$ curl -v 93.184.216.34:8000
* processing: 93.184.216.34:8000
* Trying 93.184.216.34:8000...
```
However, currently, we get no response since the firewall of 93.184.216.34 drops the incoming packets.
We can also see this in the captured network traffic since there are no further packets:
_10.172.16.1:_
```
$ tcpdump -i any -n '(host 54.147.66.132 or net 10.172.16.0/24) and not port 22'
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
22:52:25.007994 eth0 In IP 54.147.66.132.47746 > 93.184.216.34.8000: Flags [S], seq 224277161, win 64240, options [mss 1452,sackOK,TS val 3994473945 ecr 0,nop,wscale 7], length 0
```
_(54.147.66.132 is the public IP address used by 10.172.16.2)_
To open port 8000 and use DNAT, we update the firewall configuration like this:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
-A FORWARD -i wg0 -o wg0 -j ACCEPT
+ -A FORWARD -i eth0 -o wg0 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
+ -t nat -A PREROUTING -p tcp -m tcp --dport 8000 -j DNAT --to-destination 10.172.16.2:8000
```
We will now see that the destination IP address of the packet is replaced <span id="ft-1b">[[1]](#ft-1)</span>:
```diff
23:00:35.336176 eth0 In IP 54.147.66.132.38308 > 93.184.216.34.8000: Flags [S], seq 511352029, win 64240, options [mss 1452,sackOK,TS val 3994964273 ecr 0,nop,wscale 7], length 0
+ 23:00:35.336226 wg0 Out IP 54.147.66.132.38308 > 10.172.16.2.8000: Flags [S], seq 511352029, win 64240, options [mss 1452,sackOK,TS val 3994964273 ecr 0,nop,wscale 7], length 0
```
As mentioned, we can't route public IP addresses inside our VPN. Therefore, we also need to use SNAT to translate the source IP address to the internal IP address:
```diff
-t nat -A PREROUTING -p tcp -m tcp --dport 8000 -j DNAT --to-destination 10.172.16.2:8000
+ -t nat -A POSTROUTING -d 10.172.16.2/32 -o wg0 -p tcp -m tcp --dport 8000 -j SNAT --to-source 10.172.16.1
```
We now see that the source IP address is also translated and further packets are captured:
```diff
23:14:28.136307 eth0 In IP 54.147.66.132.54866 > 93.184.216.34.8000: Flags [S], seq 1430183439, win 64240, options [mss 1452,sackOK,TS val 3995797073 ecr 0,nop,wscale 7], length 0
- 23:14:28.136339 wg0 Out IP 54.147.66.132.54866 > 10.172.16.2.8000: Flags [S], seq 1430183439, win 64240, options [mss 1452,sackOK,TS val 3995797073 ecr 0,nop,wscale 7], length 0
+ 23:14:28.136339 wg0 Out IP 10.172.16.1.54866 > 10.172.16.2.8000: Flags [S], seq 1430183439, win 64240, options [mss 1452,sackOK,TS val 3995797073 ecr 0,nop,wscale 7], length 0
+ 23:14:28.136375 eth0 Out IP 93.184.216.34.51913 > 54.147.66.132.38785: UDP, length 96
+ 23:14:28.146021 eth0 In IP 54.147.66.132.38785 > 93.184.216.34.51913: UDP, length 96
+ 23:14:28.146053 wg0 In IP 10.172.16.2.8000 > 10.172.16.1.54866: Flags [S.], seq 3292104528, ack 1430183440, win 64296, options [mss 1380,sackOK,TS val 445665275 ecr 3995797073,nop,wscale 7], length 0
```
The last two packets are the response from the HTTP server. However, the response does not get forwarded to the <code class="bg-transparent">eth0</code> interface. That's because we only added a rule to allow forwarding from <code class="bg-transparent">eth0</code> to <code class="bg-transparent">wg0</code> but not vice versa. After changing this:
```diff
-A FORWARD -i eth0 -o wg0 -j ACCEPT
+ -A FORWARD -i wg0 -o eth0 -j ACCEPT
```
we receive the response now:
```diff
23:31:36.029921 eth0 In IP 54.147.66.132.42710 > 93.184.216.34.8000: Flags [S], seq 2290103144, win 64240, options [mss 1452,sackOK,TS val 3996824967 ecr 0,nop,wscale 7], length 0
23:31:36.030009 wg0 Out IP 10.172.16.1.54866 > 10.172.16.2.8000: Flags [S], seq 2290103144, win 64240, options [mss 1452,sackOK,TS val 3996824967 ecr 0,nop,wscale 7], length 0
23:31:36.030073 eth0 Out IP 93.184.216.34.59194 > 54.147.66.132.38785: UDP, length 96
23:31:36.039876 eth0 In IP 54.147.66.132.38785 > 93.184.216.34.59194: UDP, length 96
23:31:36.039924 wg0 In IP 10.172.16.2.8000 > 10.172.16.1.42710: Flags [S.], seq 4227529946, ack 2290103145, win 64296, options [mss 1380,sackOK,TS val 446693169 ecr 3996824967,nop,wscale 7], length 0
+ 23:31:36.039947 eth0 Out IP 93.184.216.34.8000 > 54.147.66.132.42710: Flags [S.], seq 4227529946, ack 2290103145, win 64296, options [mss 1380,sackOK,TS val 446693169 ecr 3996824967,nop,wscale 7], length 0
```
The output of <code class="bg-transparent">curl</code> further confirms this:
_10.172.16.2_:
```
$ curl -I 93.184.216.34:8000
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.11.5
Date: Sun, 24 Sep 2023 23:36:26 GMT
Content-type: text/html; charset=utf-8
Content-Length: 187
```
<a class="header" name="port-forwarding-final">
## final configuration
</a>
The final firewall configuration of 10.172.16.1 is this:
```diff
-P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT
+ -A FORWARD -i eth0 -o wg0 -j ACCEPT
+ -A FORWARD -i wg0 -o eth0 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
-A OUTPUT -o wg0 -j ACCEPT
+ -t nat -A PREROUTING -p tcp -m tcp --dport 8000 -j DNAT --to-destination 10.172.16.2:8000
+ -t nat -A POSTROUTING -d 10.172.16.2/32 -o wg0 -p tcp -m tcp --dport 8000 -j SNAT --to-source 10.172.16.1
```
---
Congratulations! You now know how network address translation works and how port forwarding is implemented using DNAT and SNAT.
---
<small>
<span id="ft-0">[[0]](#ft-0b) A router in a private network has two IP addresses: a private address which is commonly the first IP address in the used ranged (so 192.168.0.1 if the range 192.168.0.0/16 is used) and a public one assigned by an internet service provider.
</span><br />
<span id="ft-1">[[1]](#ft-1b) We can see that it's the same packet since everything is the same (especially the sequence number and TS val) except the destination IP address.
</small>

View File

@ -1,20 +0,0 @@
<code>
<strong>
<pre class="text-center">
_ _
| |__ | | ___ __ _
| '_ \| |/ _ \ / _` |
| |_) | | (_) | (_| |
|_.__/|_|\___/ \__, |
|___/ </pre>
</strong>
</code>
<ul>
{{ range .Posts -}}
<li>
<span class="font-mono mb-1">{{- .Date -}}</span> |
<a href="{{- ToHref .FsPath -}}">{{- .Title -}}</a> |
<span>{{- .Sats }} sats</span>
</li>
{{- end }}
</ul>

25
content/about.md Normal file
View File

@ -0,0 +1,25 @@
+++
title = "About"
date = "2024-04-13"
[params]
banner = "psychedelic-digital-sky.jpg"
+++
Hi, I'm ek. I guess this is the part where I introduce myself in an interesting and unique way such that reading this is worth your time and you don't immediately leave. [Attention is a scarce resource](https://en.wikipedia.org/wiki/Attention_economy) after all.
However, I would actually prefer to not introduce myself. I would prefer to let my words speak for themselves--my words here and in what I write about on this site. So let me introduce you to my site.
## To be unique to be authentic
I see this site as my tiny island in the vastness of the internet. It does not try to compete with other islands since I believe this would mean it's not trying to be unique. **Competition can only exist between parties that want the same thing.**
However, is this true? Even if an island is trying to be unique, from the visitor's perspective, it still competes with every other island, unwillingly or not. A visitor can only be at one island at a time after all. Additionally, the desire to be unique is not unique among islands. A visitor doesn't care what you want. A visitor cares what you provide.
Does this mean competition is a matter of perspective? Maybe so. But more importantly, it means that **competition can only exist between parties that provide the same thing.**
Therefore, the goal of this site is to provide you something unique. It eliminates competition by stepping out of it and embracing its quirks. It's very much like finding a niche in which you naturally fit in a way that others can't. **Maybe this is the ultimate competition: to be unique to be authentic.**
In any way, this whole site is meant to be a [complex search query to find fascinating people](https://www.henrikkarlsson.xyz/p/search-query). I will leave my introduction at this for now.
[Why don't you introduce yourself to me now?](https://stacker.news/items/505345)

21
deploy Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# https://gohugo.io/hosting-and-deployment/deployment-with-rsync/
set -e
RSYNC_OPTS="-avh --delete"
COMMIT=$(git rev-parse --short HEAD)
sed -i -e "s/^commit\s*=.*$/commit = \"$COMMIT\"/" hugo.toml
rm -r public/
hugo
rsync $RSYNC_OPTS --dry-run public/ ekzy.is:/var/www/ek
echo
read -p "Continue deploy? [yn] " yn
echo
[ "$yn" == "y" ] && rsync $RSYNC_OPTS public/ ekzy.is:/var/www/ek
git checkout hugo.toml

View File

@ -1,11 +0,0 @@
#!/usr/bin/env bash
set -e
ENV=production make -B
rsync -avh .well-known public/ ekzyis.com:/var/www/ekzyis --delete --dry-run
echo
read -p "Continue deploy? [yn] " yn
echo
[ "$yn" == "y" ] && rsync -avh .well-known public/ ekzyis.com:/var/www/ekzyis --delete

16
go.mod
View File

@ -1,16 +0,0 @@
module renderer
go 1.20
require github.com/tdewolff/minify/v2 v2.12.8
require (
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
github.com/namsral/flag v1.7.4-pre // indirect
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
github.com/tdewolff/parse/v2 v2.6.7 // indirect
golang.org/x/net v0.14.0 // indirect
)

59
go.sum
View File

@ -1,59 +0,0 @@
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E=
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs=
github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/tdewolff/minify/v2 v2.12.8 h1:Q2BqOTmlMjoutkuD/OPCnJUpIqrzT3nRPkw+q+KpXS0=
github.com/tdewolff/minify/v2 v2.12.8/go.mod h1:YRgk7CC21LZnbuke2fmYnCTq+zhCgpb0yJACOTUNJ1E=
github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8bBg=
github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
github.com/tdewolff/test v1.0.9 h1:SswqJCmeN4B+9gEAi/5uqT0qpi1y2/2O47V/1hhGZT0=
github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

118
html.go
View File

@ -1,118 +0,0 @@
package main
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/html"
)
var (
m *minify.M
BuildDate string
t = template.Must(template.ParseGlob("html/template/*.html"))
)
func init() {
m = minify.New()
m.AddFunc("text/html", html.Minify)
BuildDate = time.Now().In(time.UTC).Format("2006-01-02 15:04:05.000000000 -0700")
}
type HtmlSource struct {
FsPath string
Title string
Href string
Content []byte
}
func NewHtmlSource(path string) *HtmlSource {
source := HtmlSource{FsPath: path}
source.ComputeTitle()
source.ComputeHref()
source.LoadContent()
return &source
}
func (source *HtmlSource) ComputeTitle() {
// FIXME
// this is just a temporary workaround.
// actual solution would be to use title from blog post markdown metadata
switch source.FsPath {
case "html/pages/blog/index.html":
source.Title = "blog | ekzyis"
case "html/pages/blog/20230809-demystifying-wireguard-and-iptables.html":
source.Title = "Demystifying WireGuard and iptables | blog | ekzyis"
case "html/pages/blog/20230821-wireguard-packet-forwarding.html":
source.Title = "WireGuard Packet Forwarding | blog | ekzyis"
case "html/pages/blog/20230925-wireguard-port-forwarding.html":
source.Title = "WireGuard Port Forwarding | blog | ekzyis"
default:
source.Title = "ekzyis"
}
}
func (source *HtmlSource) ComputeHref() {
source.Href = strings.ReplaceAll(strings.ToLower(source.FsPath), ".html", "")
if source.Href == "index" {
source.Href = "/"
}
}
func (source *HtmlSource) LoadContent() {
content, err := os.ReadFile(source.FsPath)
if err != nil {
panic(err)
}
source.Content = content
}
func GetVersion() string {
cmd := exec.Command("git", "rev-parse", "--short", "HEAD")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
panic(err)
}
return out.String()
}
func (source *HtmlSource) Render() {
args := map[string]any{
"BuildDate": BuildDate,
"Env": Env,
"Title": source.Title,
"Href": source.Href,
"Content": string(source.Content),
"Version": GetVersion(),
}
ExecuteTemplate(args)
}
func ExecuteTemplate(args map[string]any) {
mw := m.Writer("text/html", os.Stdout)
defer mw.Close()
err := t.ExecuteTemplate(mw, "layout.html", args)
if err != nil {
panic(err)
}
}
func GetHtmlSources(srcDir string) *[]HtmlSource {
var sources []HtmlSource
paths, err := filepath.Glob(srcDir + "*.html")
if err != nil {
panic(err)
}
for _, path := range paths {
sources = append(sources, *NewHtmlSource(path))
}
return &sources
}

View File

@ -1,11 +0,0 @@
<code>
<strong>
<pre class="text-center">
_ _ ___ _ _
| || | / _ \| || |
| || |_| | | | || |_
|__ _| |_| |__ _|
|_| \___/ |_| </pre>
</strong>
</code>
<div class="font-mono mb-1 text-center">Not Found</div>

View File

@ -1,66 +0,0 @@
<code>
<strong>
<pre class="text-center">
_ _
___| | __ _____ _(_)___
/ _ \ |/ /|_ / | | | / __|
| __/ < / /| |_| | \__ \
\___|_|\_\/___|\__, |_|___/
|___/ </pre>
</strong>
</code>
<div class="text-center">
<p>
<small>
<i>
just a pleb who likes programming, networking, security, CTFs, privacy, bitcoin, or basically anything that is
tech related ...
</i>
</small>
</p>
</div>
<div class="font-mono mb-1 text-center">Welcome!</div>
<div>
<p>
I see this website as my personal playground where I can experiment with (web) technologies.<br />
For example, I intend to host this site as a onion site and integrate protocols like
<a href="https://docs.lightning.engineering/the-lightning-network/l402">L402</a> or
<a href="https://nostr.com/">nostr</a>.
</p>
<p>
I also want to see with this site how far one can go with only static HTML and CSS; no JS.<sup>*</sup><br />
The static HTML files are generated with
<a href="https://pkg.go.dev/text/template">text/template</a>
and deployed with
<a href="https://wiki.archlinux.org/title/rsync">rsync</a>.
</p>
<p>
Additionally, I want to share any knowledge gained in
<a href="/blog">blog posts</a> for the following reasons:
<ul>
<li>They might be useful as guides if others want to do similar things</li>
<li>
Knowing that I can share whatever I learned with others later helps me stay motivated to keep learning more
</li>
<li>I want to keep track of all the things I've learned over time</li>
<li>
I would love to talk more about topics that I am interested in with people
(<a href="https://stacker.news/">stacker.news</a> made me realize this!)
</li>
<li>
Blogging is an obvious use case for a personal website and I want to be creative and write more
</li>
</ul>
<p class="text-center">
If I caught your interest now, you can subscribe via <a href="/blog/rss.xml">RSS</a>!
</p>
<p>
<p class="text-center"><small>[subscribing via email and nostr coming soon]</small></p>
<p class="text-center">
<small>
<sup>*</sup> i use
<a target="_blank" href="https://plausible.io/ekzyis.com/">plausible.io</a>
for web analytics though
</small>
</p>
</div>

View File

@ -1,15 +0,0 @@
<footer class="flex text-center justify-center mx-1 pb-1">
<div>
<hr />
<nav class="flex flex-row text-center justify-center">
<a href="/blog/rss.xml">rss</a>
<a href="https://primal.net/p/npub16x07c4qz05yhqe2gy2q2u9ax359d2lc0tsh6wn3y70dmk8nv2j2s96s89d">nostr</a>
<a href="/simplex.jpeg">simplex</a>
<a href="/pgp.txt">pgp</a>
<a href="https://git.ekzyis.com/ekzyis">git</a>
<a href="https://stacker.news/ekzyis">sn</a>
</nav>
<small><code>Build Date: {{ .BuildDate }}</code></small><br />
<small><code>running <a target="_blank" href="https://git.ekzyis.com/ekzyis/ekzyis/commit/{{ .Version }}">{{ .Version }}</a></code></small>
</div>
</footer>

View File

@ -1,17 +0,0 @@
<head>
<title>{{ if .Title }} {{- .Title -}} {{ else }} {{- "ekzyis" -}} {{ end }}</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="stylesheet" href="/index.css" />
<link rel="stylesheet" href="/desert.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#091833" />
<link rel="alternate" type="application/rss+xml" title="blog | ekzyis" href="/blog/rss.xml" />
<script defer data-api="/api/event" data-domain="ekzyis.com" src="/js/script.js"></script>
{{ if eq .Env "development" }}
<script defer src="/hotreload.js"></script>
{{ end }}
</head>

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html>
{{ template "head.html" . }}
<body>
<header class="flex flex-row text-center justify-center pt-1 mx-1">
{{ template "nav.html" }}
</header>
<div class="container flex flex-column">
{{ .Content }}
</div>
{{ template "footer.html" . }}
</body>
</html>

View File

@ -1,4 +0,0 @@
<nav>
<a href="/">home</a>
<a href="/blog">blog</a>
</nav>

14
hugo.toml Normal file
View File

@ -0,0 +1,14 @@
baseURL = 'https://ekzy.is/'
languageCode = 'en-us'
title = 'ekzyis'
theme = 'holy'
[menu]
[[menu.main]]
identifier = "about"
name = "About"
url = "/about/"
weight = 10
[params]
commit = "000000"

7
layouts/404.html Normal file
View File

@ -0,0 +1,7 @@
{{ define "main" }}
<main id="main">
<div>
404 Not Found
</div>
</main>
{{ end }}

View File

@ -0,0 +1 @@
<script defer data-domain="ekzy.is" src="/js/script.js"></script>

View File

@ -1,171 +0,0 @@
package main
import (
"bufio"
"bytes"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
"github.com/PuerkitoBio/goquery"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
)
var (
MarkdownToHtmlFlags = (html.CommonFlags ^ html.SmartypantsFractions) | html.HrefTargetBlank
)
type MarkdownPost struct {
// file system path
FsPath string
// markdown content
Content []byte
// args parsed from markdown
Title string
Date string
ReadingTime string
Sats int
Comments string
}
func NewMarkdownPost(path string) *MarkdownPost {
post := MarkdownPost{FsPath: path}
post.LoadContent()
return &post
}
func (post *MarkdownPost) LoadContent() {
f, err := os.OpenFile(post.FsPath, os.O_RDONLY, 0755)
if err != nil {
panic(err)
}
sc := bufio.NewScanner(f)
post.ParseArgs(sc)
var content []byte
for sc.Scan() {
read := sc.Bytes()
content = append(content, read...)
content = append(content, '\n')
}
err = sc.Err()
if err != nil {
panic(err)
}
post.Content = content
}
func (post *MarkdownPost) ParseArgs(sc *bufio.Scanner) {
for sc.Scan() {
line := sc.Text()
parts := strings.Split(line, ":")
if len(parts) < 2 {
break
}
parts[1] = strings.Trim(parts[1], " \n")
switch parts[0] {
case "Title":
post.Title = strings.Join(parts[1:], ":")
case "Date":
post.Date = parts[1]
case "ReadingTime":
post.ReadingTime = parts[1]
case "Sats":
sats, err := strconv.Atoi(parts[1])
if err != nil {
panic(err)
}
post.Sats = int(sats)
case "Comments":
post.Comments = strings.Join(parts[1:], ":")
}
}
err := sc.Err()
if err != nil {
panic(err)
}
}
func (post *MarkdownPost) InsertHeader(htmlContent *[]byte) {
header := []byte("" +
"<code class=\"bg-transparent\"><strong><pre class=\"bg-transparent text-center\">\n" +
" _ _ \n" +
"| |__ | | ___ __ _ \n" +
"| '_ \\| |/ _ \\ / _` |\n" +
"| |_) | | (_) | (_| |\n" +
"|_.__/|_|\\___/ \\__, |\n" +
" |___/ </pre></strong></code>\n" +
"<div><div class=\"font-mono mb-1 text-center\">\n" +
"<strong>{{- .Title }}</strong><br />\n" +
"<small>{{- .Date }} | {{ .ReadingTime }} | <a target=\"_blank\" href=\"{{ .Comments }}\">Comments</a> | {{ .Sats }} sats</small>\n" +
"</div>\n")
*htmlContent = append(header, *htmlContent...)
}
func (post *MarkdownPost) StyleHtml(htmlContent *[]byte) {
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(*htmlContent))
if err != nil {
panic(err)
}
doc.Find("img").Each(func(index int, element *goquery.Selection) {
element.AddClass("flex m-auto")
})
doc.Find("pre, code").Each(func(index int, element *goquery.Selection) {
element.AddClass("code")
})
doc.Find("code[class*=\"language-\"]").Each(func(index int, element *goquery.Selection) {
SyntaxHighlighting(element)
})
htmlS, err := doc.Html()
if err != nil {
panic(err)
}
*htmlContent = []byte(htmlS)
}
func GetPosts(srcDir string) *[]MarkdownPost {
paths, err := filepath.Glob(srcDir + "*.md")
if err != nil {
panic(err)
}
var posts []MarkdownPost
for _, path := range paths {
post := NewMarkdownPost(path)
posts = append(posts, *post)
}
return &posts
}
func (post *MarkdownPost) Render() {
opts := html.RendererOptions{Flags: MarkdownToHtmlFlags}
renderer := html.NewRenderer(opts)
html := markdown.ToHTML(post.Content, nil, renderer)
post.InsertHeader(&html)
post.StyleHtml(&html)
t, err := template.New("post").Parse(string(html))
if err != nil {
panic(err)
}
t.Execute(os.Stdout, *post)
}
func RenderBlogIndex(srcDir string) {
posts := GetPosts(srcDir)
srcPath := srcDir + "index.html"
t := template.New(filepath.Base(srcPath))
t = t.Funcs(template.FuncMap{
"ToHref": func(fsPath string) string {
return "/" + strings.ReplaceAll(fsPath, ".md", "")
},
})
t, err := t.ParseFiles(srcPath)
if err != nil {
panic(err)
}
err = t.Execute(os.Stdout, map[string][]MarkdownPost{"Posts": *posts})
if err != nil {
panic(err)
}
}

View File

@ -1,57 +0,0 @@
proxy_cache_path /var/run/nginx-cache/jscache levels=1:2 keys_zone=jscache:100m inactive=30d use_temp_path=off max_size=100m;
server {
server_name ekzyis.com;
listen 80;
listen [::]:80;
return 301 https://ekzyis.com$request_uri;
}
server {
server_name ekzyis.com;
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/ekzyis.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ekzyis.com/privkey.pem;
root /var/www/ekzyis;
index index.html;
try_files $uri.html $uri $uri/ =404;
error_page 404 /404.html;
resolver 9.9.9.9;
set $plausible_script_url https://plausible.io/js/script.js;
set $plausible_event_url https://plausible.io/api/event;
location = /js/script.js {
proxy_pass $plausible_script_url;
proxy_set_header Host plausible.io;
# Tiny, negligible performance improvement. Very optional.
proxy_buffering on;
# Cache the script for 6 hours, as long as plausible.io returns a valid response
proxy_cache jscache;
proxy_cache_valid 200 6h;
proxy_cache_use_stale updating error timeout invalid_header http_500;
# Optional. Adds a header to tell if you got a cache hit or miss
add_header X-Cache $upstream_cache_status;
}
location = /api/event {
proxy_pass $plausible_event_url;
proxy_set_header Host plausible.io;
proxy_buffering on;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
include letsencrypt.conf;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="https://ekzyis.com/blog/rss.xml" rel="self" type="application/rss+xml" />
<title>blog | ekzyis</title>
<link>https://ekzyis.com/blog</link>
<description>Blog posts from ekzyis</description>
<item>
<guid>https://ekzyis.com/blog/20230809-demystifying-wireguard-and-iptables</guid>
<title>Demystifying WireGuard and iptables</title>
<link>https://ekzyis.com/blog/20230809-demystifying-wireguard-and-iptables</link>
<comments>https://stacker.news/items/221471</comments>
</item>
<item>
<guid>https://ekzyis.com/blog/20230821-wireguard-packet-forwarding</guid>
<title>WireGuard Packet Forwarding</title>
<link>https://ekzyis.com/blog/20230821-wireguard-packet-forwarding</link>
<comments>https://stacker.news/items/230179</comments>
</item>
<item>
<guid>https://ekzyis.com/blog/20230925-wireguard-port-forwarding</guid>
<title>WireGuard Port Forwarding</title>
<link>https://ekzyis.com/blog/20230925-wireguard-port-forwarding</link>
<comments>https://stacker.news/items/265524</comments>
</item>
</channel>
</rss>

View File

@ -1,34 +0,0 @@
/* desert scheme ported from vim to google prettify */
pre.prettyprint { display: block; background-color: #333 }
pre .nocode { background-color: none; color: #000 }
pre .str { color: #ffa0a0 } /* string - pink */
pre .kwd { color: #f0e68c; font-weight: bold }
pre .com { color: #87ceeb } /* comment - skyblue */
pre .typ { color: #98fb98 } /* type - lightgreen */
pre .lit { color: #cd5c5c } /* literal - darkred */
pre .pun { color: #fff } /* punctuation */
pre .pln { color: #fff } /* plaintext */
pre .tag { color: #f0e68c; font-weight: bold } /* html/xml tag - lightyellow */
pre .atn { color: #bdb76b; font-weight: bold } /* attribute name - khaki */
pre .atv { color: #ffa0a0 } /* attribute value - pink */
pre .dec { color: #98fb98 } /* decimal - lightgreen */
/* Specify class=linenums on a pre to get line numbering */
ol.linenums { margin-top: 0; margin-bottom: 0; color: #AEAEAE } /* IE indents via margin-left */
li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8 { list-style-type: none }
/* Alternate shading for lines */
li.L1,li.L3,li.L5,li.L7,li.L9 { }
@media print {
pre.prettyprint { background-color: none }
pre .str, code .str { color: #060 }
pre .kwd, code .kwd { color: #006; font-weight: bold }
pre .com, code .com { color: #600; font-style: italic }
pre .typ, code .typ { color: #404; font-weight: bold }
pre .lit, code .lit { color: #044 }
pre .pun, code .pun { color: #440 }
pre .pln, code .pln { color: #000 }
pre .tag, code .tag { color: #006; font-weight: bold }
pre .atn, code .atn { color: #404 }
pre .atv, code .atv { color: #060 }
}

View File

@ -1,64 +0,0 @@
/* Doxy pretty-printing styles. Used with prettify.js. */
pre .str, code .str { color: #fec243; } /* string - eggyolk gold */
pre .kwd, code .kwd { color: #8470FF; } /* keyword - light slate blue */
pre .com, code .com { color: #32cd32; font-style: italic; } /* comment - green */
pre .typ, code .typ { color: #6ecbcc; } /* type - turq green */
pre .lit, code .lit { color: #d06; } /* literal - cherry red */
pre .pun, code .pun { color: #8B8970; } /* punctuation - lemon chiffon4 */
pre .pln, code .pln { color: #f0f0f0; } /* plaintext - white */
pre .tag, code .tag { color: #9c9cff; } /* html/xml tag (bluey) */
pre .htm, code .htm { color: #dda0dd; } /* html tag light purply*/
pre .xsl, code .xsl { color: #d0a0d0; } /* xslt tag light purply*/
pre .atn, code .atn { color: #46eeee; font-weight: normal;} /* html/xml attribute name - lt turquoise */
pre .atv, code .atv { color: #EEB4B4; } /* html/xml attribute value - rosy brown2 */
pre .dec, code .dec { color: #3387CC; } /* decimal - blue */
a {
text-decoration: none;
}
pre.prettyprint, code.prettyprint {
font-family:'Droid Sans Mono','CPMono_v07 Bold','Droid Sans';
font-weight: bold;
font-size: 9pt;
background-color: #0f0f0f;
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
-o-border-radius: 8px;
-ms-border-radius: 8px;
-khtml-border-radius: 8px;
border-radius: 8px;
} /* background is black (well, just a tad less dark ) */
pre.prettyprint {
width: 95%;
margin: 1em auto;
padding: 1em;
white-space: pre-wrap;
}
pre.prettyprint a, code.prettyprint a {
text-decoration:none;
}
/* Specify class=linenums on a pre to get line numbering; line numbers themselves are the same color as punctuation */
ol.linenums { margin-top: 0; margin-bottom: 0; color: #8B8970; } /* IE indents via margin-left */
li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8 { list-style-type: none }
/* Alternate shading for lines */
li.L1,li.L3,li.L5,li.L7,li.L9 { }
/* print is mostly unchanged from default at present */
@media print {
pre.prettyprint, code.prettyprint { background-color: #fff; }
pre .str, code .str { color: #088; }
pre .kwd, code .kwd { color: #006; font-weight: bold; }
pre .com, code .com { color: #0c3; font-style: italic; }
pre .typ, code .typ { color: #404; font-weight: bold; }
pre .lit, code .lit { color: #044; }
pre .pun, code .pun { color: #440; }
pre .pln, code .pln { color: #000; }
pre .tag, code .tag { color: #b66ff7; font-weight: bold; }
pre .htm, code .htm { color: #606; font-weight: bold; }
pre .xsl, code .xsl { color: #606; font-weight: bold; }
pre .atn, code .atn { color: #c71585; font-weight: normal; }
pre .atv, code .atv { color: #088; font-weight: normal; }
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,16 +0,0 @@
const scroll = (y) => window.scrollTo(0, 925*y)
async function hotReload() {
console.log("running in development mode")
const r = await fetch("/hot-reload")
let x = await r.text()
setInterval(async () => {
const r = await fetch("/hot-reload", {
cache: "no-cache"
})
if (x !== await r.text()) {
x = r.body
window.location.reload()
}
}, 1000)
}
hotReload().catch(console.error)

View File

@ -1,170 +0,0 @@
body {
background-color: #091833;
color: #ffffff;
caret-color: transparent;
font-family: Arial, Helvetica, sans-serif;
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
a {
color: #8787a4;
text-decoration: underline;
width: fit-content;
}
a:hover {
background: #8787A4;
color: #ffffff;
}
a > code:hover {
background-color: #8787A4;
}
a.header {
color: inherit;
text-decoration: none;
}
a.subh {
padding: 0 .3em;
}
a.header:hover {
color: inherit;
}
nav > a {
margin: 0 3px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.3em;
}
h3 {
font-size: 1.1em;
font-style: italic;
}
hr {
width: 100%;
border: none;
height: 1px;
background: repeating-linear-gradient(90deg,#fff,#fff 6px,transparent 1px,transparent 8px);
}
ul {
text-align: left;
}
img {
max-width: 100%;
height: auto;
}
pre {
overflow-x: auto;
padding: 0.5em;
}
.code {
background-color: #2b3148;
}
.diff-remove {
color: #ed88a5;
}
.diff-add {
color: #a2de88;
}
.container {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
margin: 1em auto;
padding: 0 1em;
max-width: min(60rem, 100vw);
}
.block {
display: block;
}
.flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.font-bold {
font: bold;
}
.font-mono {
font-family: "Lucida Console", "Courier New", monospace;
}
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}
.h-100vh {
height: 100vh;
}
.mx-1 {
margin: auto 1em;
}
.m-auto {
margin: auto;
}
.mb-1 {
margin-bottom: 1em;
}
.mt-1 {
margin-top: 1em;
}
.pt-1 {
padding-top: 1em;
}
.pb-1 {
padding-bottom: 1em;
}
.bg-transparent {
background-color: transparent !important;
}
details > summary {
list-style: none;
color: #8787a4;
text-decoration: underline;
width: fit-content;
cursor: pointer;
}
details > summary:hover {
background: #8787A4;
color: #ffffff;
}
details > summary > code:hover {
background-color: #8787A4;
}
details > summary::-webkit-details-marker {
display: none;
}

View File

@ -1,76 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: Keybase OpenPGP v2.1.13
Comment: https://keybase.io/ekzyis
xsFNBGXdXusBEACrWhcL8nU8Nz1osqBqnnqSX08YDniBltW9Y+hsmPxR3qjFyu3J
rL7+j6OKvtMuxESbgoDbRK2zUY0vMCGV+hesRk2puZc6sGN3euwHHzTRRvKgoqV2
NXO66CKQBfp6tKNGdwzgiqIjUeZC2viMogPhhCoyGuMKGfHOtLoNFjZ/gcMzDcSV
yhkL2w3WcwgzZ4s5v85QkwLCYXUO70/Wnv8iVUNt4IXT1mD87ZpDvKZ1s/66I22l
l65yDVhqpZBSN8eymz1NWht2siCXQMy5foyiIhQ/x8hwYP6TyvTtgSMzQ7RClwYU
WVS30V6+MlErZKIYAJcNqCTQOyaklkhewVAyW717b/E/c9GH5+sM62fePDpuNl7f
INHbFFp0rkiPJAL9HTISU9UVbe8Dpujy0z+yRU8TCqNZRfL5YH+X0EcGnd6soWEp
tQHQqQbnVKnOJ/vFW0wBbi0N6SfQnScnMMOUgDkvg6My1YrJKqnEtJG89OpmAvjb
LWnSGyBCcJh5KAAUYqNkDYtSm5fyhQkQIiD/c00KCAs/w/+1YEHJN3OmFADtxm76
m/XT+JBLkCKhKacR8RYxs/YkuVtlhMT7qzBAu8Q4zvwKwkUWPGmwWaxkmYBb9cfo
KEKzfvrzrB2F98AFN1H6ruWOAXpqtQ3Uu8mcmTF7YNYt1L53z9uk/OdKRwARAQAB
zRpla3p5aXMgPGVrenlpc0Bla3p5aXMuY29tPsLBdAQTAQoAHgUCZd1e6wIbAwML
CQcDFQoIAh4BAheAAxYCAQIZAQAKCRDrr3XacnnLSCvGD/9QuSd9QhvW8eHsyjDO
EK2r06VcftnBAmG4YAEIcq//5rm426AqS1WjjCRJOd/Jho+ovLAljS3eScn/axaE
t74preuZslQALpoNDw7h/Il9B5IoOyoMhkYFT51Y8b11ciXZisUOaFOeljwogxax
20C+EillF0qhOcQzvGXWrLOuKmO42qUyGEdhrUeV6lrjHanevoTw6PASwk/rplQW
zgq7/bUePsOwBg3oqq8v1U2uNUU02EJsC/QgMfDRFaiN3ukdplJ7obqoQjB6l6sJ
JRA7VNTZCJOq4q3YpJCxQVIMa8F5QXAUwY89bZmsXJ88fQ1ajL88JpXHgqnVLlHD
lzYZRNCZyt1EfsMAvCx0BhJ37vZ8FbcDKnf3WvEj1vrCiWFd/QdlIbyK1tuTHLQV
xuDQN/yZnjVSQQbIYVHyRdiLqzPfV5LQa0Htad5XXKY0tlJOAYYfn9z5jHkh/9F9
SxMkUykXYBwfrSUkpGYenlwMfiKERssbtutBpSPyjaeKzcq+t6HhqDPFEkoyh6wh
FaVJP2/BjEMb75Uw0xQttgG8mWMoowldcl+aUav4t2Zx4HcdHQH8NxTQM7tefbxF
s+VdXz0EaIwX18eC9yuLT42gCKiC8PldziYzztVcodgNOVvLhPxgQ4RjCi/A1Gxv
H6DxLBS0ZDKXDOsH31BUi+nG6c7ATQRl3V7rAQgAzfN7sOogTHybBrzJ6b3C/cpD
UtKrdB8OSFmbalOza1j9fq4UaHrpcrWC6HTPdWzKYhLE7ewUPDy4YoZYccVkFVgo
QbybTTxNRrYkIsLEPaO/w27cGaaTpy3T+beqThnFwxSy7Ul7av+R92CHvZdeFbLK
zXjU17bAhh7ePkZtPxRFCXrb30cA/YvbPgkWRiSE+Hvc8DnKlA5a9+dHu/n62yEk
h7xopvX7rOYBEgsBoTgwSanP4CNQ2ZN1cdAOqLfnezCM9CjwZ6u97mDx/DroydbV
TGFmjPO3gdCwzIAT/vbVeUx4Sdzj+u0wp05k/QvHvraveRbQHeL4AqOpV2p+jQAR
AQABwsKEBBgBCgAPBQJl3V7rBQkPCZwAAhsMASkJEOuvddpyectIwF0gBBkBCgAG
BQJl3V7rAAoJEJutZGn8n9QvM3sH/3iyOUZWpEWyH4mBYuZ/5cVjG/rEdTUsappv
wJr8pmpLN2E32sCLNy/hfMiVFHw4GAgNv2z/wXEcZJqyH0QJtKg18alS0R245KFO
MLX4ZuwrwIn0DtnXQ8meaUBZMQx4OGDyOOxlnWtt9YkZRkT41RMkriYkdFXLicDM
/FpxBTk1DSmsKwbhBaXSmRDTHUA0z0LPdicqR1cPlfcSQlLJlhxmy+ef+oBiC8ZN
L43lOHfjh6V0+d1WcGKuI2JO0ca8ZPmpcAs/Wg27BOlhWMpudAsEhjYFApXQpVik
ai0RLIu0pU/lDuK4xcZyecpv3IFPyJGBzLzBBm9Rf9k6q3+ej7DaPA/+IojAk60Z
TqLKvBftGqM7P3GkVFl3M2dvpZaOBjXNI7PbyYL7CuTAa9srspJEJ7vPI3BcgkR6
DjEX4Xwouf3rKDtXPj+kyXUpJWD3fVjCjdgfcII2ClkJ6LAawfm4CBS2p1QKdENF
wo3x8I16Sj8n2ulk/k9xGLmuX/dOdFp8vsthPTBDlv9P6pjCllWLV/HPdhl+mLcr
ciNG9JvJDIBKxbC1XxcoDz4/rl6XBfxIry5yOShtjmQxFoq4qOMjVs9APOpOptc8
8A8FT/9QY45eHlJGChXqeQ7tUovwZE7PLtdqMVT8krAForJItKn7b7+1z4sTtAIp
FqGlNJKm8sWv30zCvUHZCxyphMb3gcRu6K8JgITZGNf2nzrLWlgar910OMyi1YkO
a+SsVqiuNp/e/ST9ykX1bdCq8tIq3iTfZB+mjtT5SXJOJGaric02vd4HlMzuZjUc
rJTUGqr3YPVPnld3MHGyu/JaJkD6pLjogFRQCu0k2flasiEzR3hgeJ8PFbrsdH+f
aucL1KQHu8xCLsgckKP8gCsc8njA5gJ73vfWhJ0ABR7mNa8tfWvVZqPP9P55Uyw6
Yj6VFlccWFPE0Df6io35V9LOg/yB/WE9xC+63+ju9mDJW61XLtBIxjF4SFmPAYeL
rkvfGAWrfvmHWewRTS0nBjapIWrx4ZmSRA7OwE0EZd1e6wEIAKrsNuZqHN4JE1MZ
2lyHR/zHnBMSqNSGhUUF0rYWXWu5D4ErRfUqDhLkrxcpSiIgh4pCXWfdwSUbg7MY
neOpH4KDFNRDZZ9+3gPO8ZpNSrcdDFtuJcoR/bI1JQihcvJOCAI7Ijg8caMtMeNM
8SRccJz32yX9mg8S9x+aFRjwC9VyJ20qagVDVp0W2AVPbWe+gWIP74ybgHwxJAwK
LSqZ0mlhCFi/BJec5DYqPoR478Y5ovkAk2Y7dFnH1Ic8cvtTp6C6R6SPhbQ9WVgQ
5MkbFtGBPjvVmx2T5BKr18m6cGA9k9atA+YVVOK/6cWKO+xLs9b5DKMuaP6zKhum
QR2s3DMAEQEAAcLChAQYAQoADwUCZd1e6wUJDwmcAAIbIgEpCRDrr3XacnnLSMBd
IAQZAQoABgUCZd1e6wAKCRDlEMJtiaaJVs4gB/0SOdKOGw3dIW6U2nQOS9+VauPx
GAK6nzZjWpBt1hvcCrF7cJXWNeAenQ6xThk5G5bz81Qd3OZzykm3vWNkeJnMaU1Y
7ZfsbO4y1OsS+iTE2FeBa7o4yCKdIoL8XUVINNaLL8EgsMudH9UxVpQUi26CPpZm
MhyXrFnTAaBFYIHGK0u18CepjaHEdaINlqFR2VndJpheb/HungmEPTVPld3J0B/e
g6wPwqg+tQo+dn3mNP9ZDy/tI+jcH/AcbdS86g7iyIhBBhjFpE/x5j7xlPCeJ0pz
PGt/zHZnjX6t6/L8lYUO4FATIv1teXsWl9bpp2czO4tZwyZVGXOjqfdEPsVHArEQ
AJXvJVhlZMjlquTdmNbiRNucuypvoqoYqURKQIo6U3LuM031e2jiDKvFydfmZQTD
okw0xXLptSvT+jwSZFNOojUHDjS0dxmvRSqPr5hEEGHfc6FL6OmWUAGYu+LyFc/U
Z/gmeJbhH/ChsR0MTE7HeBGWCr1OjQcXxw2MDJ2C324Cex7k6z1LbOUUyesiwWfL
QSVVO1bgwFggkn7QOgUwIgzMfO4VRifPcBKiGyP8p2uzv3lhlzjFqnX4qskuuFdR
IY6hEX6fkbnUg1WCRUx31kQsgt+6PVc4dxiLfoJo3FOC8Z0yRAGGrfj7FnoOuQLC
uED5OU6a9gXk71xuSFGxpuxO87sl0Wp1m0oVdxE9RCtdsc9ASkvUtMsJTYv3iAi8
iML9kHRczICwreeawoLQKnjVGEv6tN+uLV5XEfkqjluZLc5YHpbwUhSv9rA1RP5U
ie6ifYf2YQ8Th5VkME2XUtdPhrXVC+YcXdCsN4MM8zlHn2t9jYIx6+OePC6mymSj
+M153nHkH9S67NaD9PAouM/6B50WAH77LQQr4puwMuWp75FzcCx01NC08/Hi2YtC
I+9x1jsYc2xT6ou3CdRKlNdQY2s/CbEU2XlPLdIV1NkqAZ6GLhAeWKfPrat1ZOEj
QfYcasovMcZL3OicNiKq4cikcAAf5VRwbs+JmXe5ukkV
=Ankv
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

View File

@ -1 +0,0 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -1,118 +0,0 @@
/*
* Derived from einaros's Sons of Obsidian theme at
* http://studiostyl.es/schemes/son-of-obsidian by
* Alex Ford of CodeTunnel:
* http://CodeTunnel.com/blog/post/71/google-code-prettify-obsidian-theme
*/
.str
{
color: #EC7600;
}
.kwd
{
color: #93C763;
}
.com
{
color: #66747B;
}
.typ
{
color: #678CB1;
}
.lit
{
color: #FACD22;
}
.pun
{
color: #F1F2F3;
}
.pln
{
color: #F1F2F3;
}
.tag
{
color: #8AC763;
}
.atn
{
color: #E0E2E4;
}
.atv
{
color: #EC7600;
}
.dec
{
color: purple;
}
pre.prettyprint
{
border: 0px solid #888;
}
ol.linenums
{
margin-top: 0;
margin-bottom: 0;
}
.prettyprint {
background: #000;
}
li.L0, li.L1, li.L2, li.L3, li.L4, li.L5, li.L6, li.L7, li.L8, li.L9
{
color: #555;
list-style-type: decimal;
}
li.L1, li.L3, li.L5, li.L7, li.L9 {
background: #111;
}
@media print
{
.str
{
color: #060;
}
.kwd
{
color: #006;
font-weight: bold;
}
.com
{
color: #600;
font-style: italic;
}
.typ
{
color: #404;
font-weight: bold;
}
.lit
{
color: #044;
}
.pun
{
color: #440;
}
.pln
{
color: #000;
}
.tag
{
color: #006;
font-weight: bold;
}
.atn
{
color: #404;
}
.atv
{
color: #060;
}
}

View File

@ -1,46 +0,0 @@
/* Pretty printing styles. Used with prettify.js. */
/* Vim sunburst theme by David Leibovic */
pre .str, code .str { color: #65B042; } /* string - green */
pre .kwd, code .kwd { color: #E28964; } /* keyword - dark pink */
pre .com, code .com { color: #AEAEAE; font-style: italic; } /* comment - gray */
pre .typ, code .typ { color: #89bdff; } /* type - light blue */
pre .lit, code .lit { color: #3387CC; } /* literal - blue */
pre .pun, code .pun { color: #fff; } /* punctuation - white */
pre .pln, code .pln { color: #fff; } /* plaintext - white */
pre .tag, code .tag { color: #89bdff; } /* html/xml tag - light blue */
pre .atn, code .atn { color: #bdb76b; } /* html/xml attribute name - khaki */
pre .atv, code .atv { color: #65B042; } /* html/xml attribute value - green */
pre .dec, code .dec { color: #3387CC; } /* decimal - blue */
pre.prettyprint, code.prettyprint {
background-color: #000;
border-radius: 8px;
}
pre.prettyprint {
width: 95%;
margin: 1em auto;
padding: 1em;
white-space: pre-wrap;
}
/* Specify class=linenums on a pre to get line numbering */
ol.linenums { margin-top: 0; margin-bottom: 0; color: #AEAEAE; } /* IE indents via margin-left */
li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8 { list-style-type: none }
/* Alternate shading for lines */
li.L1,li.L3,li.L5,li.L7,li.L9 { }
@media print {
pre .str, code .str { color: #060; }
pre .kwd, code .kwd { color: #006; font-weight: bold; }
pre .com, code .com { color: #600; font-style: italic; }
pre .typ, code .typ { color: #404; font-weight: bold; }
pre .lit, code .lit { color: #044; }
pre .pun, code .pun { color: #440; }
pre .pln, code .pln { color: #000; }
pre .tag, code .tag { color: #006; font-weight: bold; }
pre .atn, code .atn { color: #404; }
pre .atv, code .atv { color: #060; }
}

View File

@ -1,34 +0,0 @@
XXXXXXXXXXXXXXXXXX
XXX Network XXX
XXXXXXXXXXXXXXXXXX
+
|
v
+-------------+ +------------------+
|table: filter| <---+ | table: nat |
|chain: INPUT | | | chain: PREROUTING|
+-----+-------+ | +--------+---------+
| | |
v | v
[local process] | **************** +--------------+
| +---------+ Routing decision +------> |table: filter |
v **************** |chain: FORWARD|
**************** +------+-------+
Routing decision |
**************** |
| |
v **************** |
+-------------+ +------> Routing decision <---------------+
|table: nat | | ****************
|chain: OUTPUT| | +
+-----+-------+ | |
| | v
v | +-------------------+
+--------------+ | | table: nat |
|table: filter | +----+ | chain: POSTROUTING|
|chain: OUTPUT | +--------+----------+
+--------------+ |
v
XXXXXXXXXXXXXXXXXX
XXX Network XXX
XXXXXXXXXXXXXXXXXX

View File

@ -1,11 +0,0 @@
+~~~~~~~~~~~~~~~~~~~~~~~~+ +~~~~~~~~~~~~~~~~~~~~~~~~~~+ +~~~~~~~~~~~~~~~~~~~~~~~~~~+
| SRC: 10.172.16.1:46541 | SNAT | SRC: 54.147.66.132:56140 | DNAT | SRC: 54.147.66.132:56140 |
| DST: 10.172.16.2:8000 |<---->| DST: 10.172.16.2:8000 |<---->| DST: 93.184.216.34:8000 |
+~~~~~~~~~~~~~~~~~~~~~~~~+ +~~~~~~~~~~~~~~~~~~~~~~~~~~+ +~~~~~~~~~~~~~~~~~~~~~~~~~~+
| |
+---+ +-------+
| |<----------------------------------------->| . . . |<-----------------------------------------> INTERNET
+---+ +-------+
10.172.16.2 NAT gateway
private IP address: 10.172.16.1
public IP address: 93.184.216.34

View File

@ -1,10 +0,0 @@
+~~~~~~~~~~~~~~~~~~~~~~~~~+ +~~~~~~~~~~~~~~~~~~~~~~~~~~+
| SRC: 54.147.66.132:443 | | SRC: 54.147.66.132:443 |
| DST: 192.168.0.14:56781 | | DST: 93.184.216.34:41501 |
+~~~~~~~~~~~~~~~~~~~~~~~~~+ | | +~~~~~~~~~~~~~~~~~~~~~~~~~~+
+---+ packet +-------+
| |<--------------------------------| . . . |<-------------------------------- INTERNET
+---+ +-------+
192.168.0.14 NAT gateway
private IP address: 192.168.0.1
public IP address: 93.184.216.34

View File

@ -1,10 +0,0 @@
+~~~~~~~~~~~~~~~~~~~~~~~~~+ +~~~~~~~~~~~~~~~~~~~~~~~~~~+
| SRC: 192.168.0.14:56781 | | SRC: 93.184.216.34:41501 |
| DST: 54.147.66.132:443 | | DST: 54.147.66.132:443 |
+~~~~~~~~~~~~~~~~~~~~~~~~~+ | | +~~~~~~~~~~~~~~~~~~~~~~~~~~+
+---+ packet +-------+
| |-------------------------------->| . . . |--------------------------------> INTERNET
+---+ +-------+
192.168.0.14 NAT gateway
private IP address: 192.168.0.1
public IP address: 93.184.216.34

View File

@ -1,3 +0,0 @@
| Source IP address | Source Port | NAT IP address | NAT port | Destination IP address | Destination Port |
| ----------------- | ----------- | -------------- | -------- | ---------------------- | ---------------- |
| 192.168.0.14 | 56781 | 93.184.216.34 | 41501 | 54.147.66.132 | 443 |

View File

@ -1,4 +0,0 @@
+---+ +---+
| |<------->| |
+---+ +---+
10.172.16.1 10.172.16.2

View File

@ -1,19 +0,0 @@
+---+ +---+ +---+
| | | | | |
+---+ +---+ +---+
\ | /
\ | /
\ | /
\ | /
\ | /
+---+ +~~~~~~~~~~~~+ +---+
| |-----| router |-----| |
+---+ +~~~~~~~~~~~~+ +---+
/ 10.172.16.1 \
/ | \
/ | \
/ | \
/ | \
+---+ +---+ +---+
| | | | | |
+---+ +---+ +---+

View File

@ -1,26 +0,0 @@
+-----------------------+ +---------------------+
| ping process | | |
| v | <-- local machine boundary remote machine boundary --> | |
| +----------------+ | | +----------------+ |
| | interface: wg0 | | | | interface: wg0 | |
| +----------------+ | | +----------------+ |
| | | | ^ |
| v | | | |
| +----------+ | | +----------+ |
| | x ICMP x | <-------------- encrypted ICMP packet leaving/entering wg0 ----------> | x ICMP x | |
| +----------+ | (virtual network interface) | +----------+ |
| | | | ^ |
| | | UDP packet leaving enp3s0 | | |
| | | +------- (physical network interface) | | |
| v | | which wraps ICMP packet | | |
| +-------------------+ | v | +-----------------+ |
| | interface: enp3s0 | | +------------+ +------------+ | | interface: eth0 | |
| +-------------------+ | | UDP | | UDP | | +-----------------+ |
+---------|-------------+ |+----------+| |+----------+| +----------^----------+
| || x ICMP x || +~~~~~~~~~~+ || x ICMP x || |
+----------------->|+----------+|----->| INTERNET |----->|+----------+|--------------+
+------------+ +~~~~~~~~~~+ +------------+
^
UDP packet entering eth0 |
(physical network interface) -----------+
which wraps ICMP packet

View File

@ -1,50 +0,0 @@
package main
import (
"log"
"path"
"github.com/namsral/flag"
)
var (
Source string
Env string
)
func init() {
flag.StringVar(&Source, "src", "", "Source file")
flag.StringVar(&Env, "env", "development", "Specify for which environment files should be built")
flag.Parse()
}
func RenderExtension(path string, ext string) {
switch ext {
case ".md":
NewMarkdownPost(path).Render()
case ".html":
NewHtmlSource(path).Render()
default:
log.Fatalf("unknown extension: %s", ext)
}
}
func main() {
if Source == "" {
log.Fatal("no source given")
}
if Source == "blog/index.html" {
RenderBlogIndex("blog/")
return
}
if Source != "" {
ext := path.Ext(Source)
if ext == "" {
log.Fatal("file has no extension")
}
RenderExtension(Source, ext)
return
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,17 +0,0 @@
#!/usr/bin/env bash
function sync() {
ENV=development make render $@
date +%s.%N > public/hot-reload
rsync -avhP public/ dev.ekzyis.com:/var/www/dev.ekzyis --delete
}
function cleanup() {
rm -f public/hot-reload
}
trap cleanup EXIT
sync -B
while inotifywait -r -e modify html/ blog/ *.go; do
sync
done

View File

@ -1,34 +0,0 @@
package main
import (
"fmt"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/sourcegraph/syntaxhighlight"
)
func SyntaxHighlighting(element *goquery.Selection) {
if element.HasClass("language-diff") {
// syntaxhighlight does not support diff so we run our custom code in that case
text := strings.Split(element.Text(), "\n")
p1 := regexp.MustCompile(`^\+ `)
p2 := regexp.MustCompile(`^- `)
for i, line := range text {
if p1.MatchString(line) {
text[i] = fmt.Sprintf("<span class=\"diff-add\">%s</span>", line)
}
if p2.MatchString(line) {
text[i] = fmt.Sprintf("<span class=\"diff-remove\">%s</span>", line)
}
}
element.SetHtml(strings.Join(text, "\n"))
return
}
formatted, err := syntaxhighlight.AsHTML([]byte(element.Text()))
if err != nil {
panic(err)
}
element.SetHtml(string(formatted))
}

1
themes/holy Submodule

@ -0,0 +1 @@
Subproject commit 55f1291be9fa61867658306a103c0ea38803e7f8