Implement markdown pipeline

I can now write blog posts in markdown.

The blog index will also be automatically built now. I only need to write a blog post in markdown now for it to be automatically included.
This commit is contained in:
ekzyis 2023-08-17 05:21:45 +02:00
parent 8c6ef67442
commit bf88488586
14 changed files with 1316 additions and 900 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
html/pages/blog
public/**/*.html
hot-reload

179
blog.go Normal file
View File

@ -0,0 +1,179 @@
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.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
}
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 = 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)
}
}
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 }} | {{ .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(destDir string) {
destPath := strings.ReplaceAll(destDir+filepath.Base(post.FsPath), ".md", ".html")
f, err := os.Create(destPath)
if err != nil {
panic(err)
}
defer f.Close()
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(f, *post)
}
func RenderBlogIndex(srcDir string, destDir string, posts *[]MarkdownPost) {
srcPath := srcDir + "index.html"
destPath := destDir + "index.html"
f, err := os.Create(destPath)
if err != nil {
panic(err)
}
defer f.Close()
t := template.New(filepath.Base(srcPath))
t = t.Funcs(template.FuncMap{
"ToHref": func(fsPath string) string {
return "/" + strings.ReplaceAll(fsPath, ".md", ".html")
},
})
t, err = t.ParseFiles(srcPath)
if err != nil {
panic(err)
}
err = t.Execute(f, map[string][]MarkdownPost{"Posts": *posts})
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,921 @@
Title: Demystifying WireGuard and iptables
Date: 2023-08-09
ReadingTime: 15 minutes
Sats: 11623
---
# introduction
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.
---
# iptables primer
`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](https://stacker.news/items/221471).
---
# wireguard
## installation
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/
```
## configuration
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
```
## interface control
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.
---
# firewall configuration with iptables
## initial configuration
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.
## local OUTPUT chain configuration
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
```
## switch remote INPUT chain policy back to DROP
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
```
## remote OUTPUT chain configuration
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
```
## switch local INPUT policy back to DROP
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
```
## final configuration
_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](https://stacker.news/items/221471).
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,20 +1,20 @@
<code>
<strong>
<pre class="text-center">
_ _
_ _
| |__ | | ___ __ _
| '_ \| |/ _ \ / _` |
| |_) | | (_) | (_| |
|_.__/|_|\___/ \__, |
|___/ </pre>
|___/ </pre>
</strong>
</code>
<ul>
{{ range .Posts }}
{{ range .Posts -}}
<li>
<span class="font-mono mb-1">{{- .Date -}}</span> |
<a href="{{- .Href -}}">{{- .Title -}}</a> |
<a href="{{- ToHref .FsPath -}}">{{- .Title -}}</a> |
<span>{{- .Sats }} sats</span>
</li>
{{ end }}
{{- end }}
</ul>

108
build.go
View File

@ -1,40 +1,15 @@
package main
import (
"bytes"
"flag"
"fmt"
"os"
"strings"
"text/template"
"time"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/html"
)
type Post struct {
Date string
Title string
ReadingTime string
Sats int
Href string
}
var (
t = template.Must(template.ParseGlob("html/template/*.html"))
paths = map[string]any{
"index.html": nil,
"404.html": nil,
"blog/index.html": nil,
"blog/20230809-Demystifying-WireGuard-and-iptables.html": Post{
Date: "2023-08-09",
Title: "Demystifying WireGuard and iptables",
ReadingTime: "15 minutes",
Sats: 11623,
},
}
dev bool
dev bool
BlogSrcDir = "blog/"
BlogDstDir = "html/pages/blog/"
HtmlSrcDirs = []string{"html/pages/", "html/pages/blog/"}
HtmlTargetDirs = []string{"public/", "public/blog/"}
)
func init() {
@ -42,66 +17,17 @@ func init() {
flag.Parse()
}
func getPosts() []Post {
var posts []Post
for path, args := range paths {
post, ok := args.(Post)
if !ok {
continue
}
post.Href = "/" + strings.ReplaceAll(strings.ToLower(path), ".html", "")
posts = append(posts, post)
}
return posts
}
func buildFiles() {
m := minify.New()
m.AddFunc("text/html", html.Minify)
buildDate := time.Now().In(time.UTC).Format("2006-01-02 15:04:05.000000000 -0700")
env := "production"
if dev {
env = "development"
}
for path, pathArgs := range paths {
htmlTitle := "ekzyis"
if path == "blog/index.html" {
htmlTitle = "blog | ekzyis"
pathArgs = map[string]any{"Posts": getPosts()}
}
if post, ok := pathArgs.(Post); ok {
htmlTitle = post.Title
}
tmp, err := template.ParseFiles(fmt.Sprintf("html/pages/%s", path))
if err != nil {
panic(err)
}
buf := new(bytes.Buffer)
tmp.Execute(buf, pathArgs)
path = strings.ToLower(path)
file, err := os.Create(fmt.Sprintf("public/%s", path))
if err != nil {
panic(err)
}
defer file.Close()
rootArgs := map[string]any{
"Title": htmlTitle,
"Body": buf.String(),
"BuildDate": buildDate,
"Env": env,
}
mw := m.Writer("text/html", file)
defer mw.Close()
err = t.ExecuteTemplate(mw, "layout.html", rootArgs)
if err != nil {
panic(err)
}
}
}
func main() {
buildFiles()
posts := GetPosts(BlogSrcDir)
for _, post := range *posts {
post.Render(BlogDstDir)
}
RenderBlogIndex(BlogSrcDir, BlogDstDir, posts)
// Go does not support ** globstar ...
// https://github.com/golang/go/issues/11862
for i, srcDir := range HtmlSrcDirs {
for _, source := range *GetHtmlSources(srcDir) {
source.Render(HtmlTargetDirs[i])
}
}
}

8
go.mod
View File

@ -4,4 +4,10 @@ go 1.20
require github.com/tdewolff/minify/v2 v2.12.8
require github.com/tdewolff/parse/v2 v2.6.7 // indirect
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/tdewolff/parse/v2 v2.6.7 // indirect
golang.org/x/net v0.14.0 // indirect
)

47
go.sum
View File

@ -1,6 +1,53 @@
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/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=

104
html.go Normal file
View File

@ -0,0 +1,104 @@
package main
import (
"os"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/html"
)
var (
m *minify.M
BuildDate string
Env 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")
Env = "production"
if dev {
Env = "development"
}
}
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() {
source.Title = "ekzyis"
if source.FsPath == "blog/index.html" {
source.Title = "blog | 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 (source *HtmlSource) Render(destDir string) {
destPath := destDir + filepath.Base(source.FsPath)
args := map[string]any{
"BuildDate": BuildDate,
"Env": Env,
"Title": source.Title,
"Href": source.Href,
"Content": string(source.Content),
}
ExecuteTemplate(destPath, args)
}
func ExecuteTemplate(destPath string, args map[string]any) {
file, err := os.Create(destPath)
if err != nil {
panic(err)
}
defer file.Close()
mw := m.Writer("text/html", file)
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,800 +0,0 @@
<code>
<strong>
<pre class="text-center">
_ _
| |__ | | ___ __ _
| '_ \| |/ _ \ / _` |
| |_) | | (_) | (_| |
|_.__/|_|\___/ \__, |
|___/ </pre>
</strong>
</code>
<div>
<div class="font-mono mb-1 text-center">
<strong>{{- .Title -}}</strong><br />
<small>{{- .Date }} | {{ .ReadingTime }} | {{ .Sats }} sats</small>
</div>
<hr />
<div class="text-left mb-1">
<h1>introduction</h1>
<p>
In this blog post, I will show you how to setup
<a target="_blank" href="https://wireguard.com">WireGuard</a>
and configure your Linux firewall with
<a target="_blank" href="https://wiki.archlinux.org/title/iptables"><code class="code">iptables</code></a>.
</p>
<P>
We will establish a point-to-point connection between two machines across the internet.<br />
One machine ("the local machine") sits behind a
<a target="_blank" href="https://en.wikipedia.org/wiki/Network_address_translation">NAT</a> 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.
</p>
<p>
In following blog articles, I will show you how to create more advanced network topologies which include VPN
gateways and port forwarding <a href="#ft-0">[0]</a>.
VPN gateways connect multiple devices together and port forwarding is usually used to expose internal services to
the internet.
</p>
<p>
It is important to me that you get a good understanding of <code class="code">iptables</code> 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.
</p>
</div>
<hr />
<div class="text-left">
<h1>iptables primer</h1>
<p>
<code class="code">iptables</code> is a command line utility for configuring Linux kernel firewalls.
</p>
<p>
It acts upon <i>tables</i> which consist of <i>chains</i> which in turn consist of <i>rules</i> and
a <i>policy</i>. The rules have criteria for packets and <i>targets</i> like <i>ACCEPT</i> or <i>DROP</i>.
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.
</p>
<p>
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 <i>filter</i>
table which contains an <i>INPUT</i>, <i>FORWARD</i> and <i>OUTPUT</i> 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 <i>nat</i>
table (which we will use for port forwarding in a future blog post) and
<a target="_blank" href="https://wiki.archlinux.org/title/iptables#Tables">three others</a>,
but they are very specialized and thus not needed in the vast majority of use cases.
</p>
<p>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
<a href="#ft-1">[1]</a>. If no matching rule was found, then the chain's policy target is used.
</p>
<p>The order of the chains is defined in this (simplified) flow chart <a href="#ft-2">[2]</a>:</p>
<img class="flex m-auto" src="/blog/img/iptables_flowchart.png" />
<p>
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>[local process]</code> and packets which are generated by local
processes enter this flow chart at <code>[local process]</code>.
</p>
<p>
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>[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.
</p>
<p>
With <code class="code">iptables -S</code> (short for <code class="code">iptables --list-rules</code>),
we can lookup the current firewall configuration for a table.
You can specify a table like this: <code class="code">iptables -t nat -S</code>.
For example, the filter table of a host inside a home network could look like this
<i>(as mentioned, if you don't specify a table, the filter table is used)</i>:
</p>
<pre class="code"><code>$ 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</code></pre>
<p>
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.
</p>
<p>
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 <i>related</i> or <i>established</i> connections.
This makes the firewall <i>stateful</i>.
</p>
<p>
Additionally, packets are logged before they are dropped by the chain policies. This uses the <i>LOG</i> 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.
</p>
<p>
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.
</p>
<p>
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
<a target="_blank" href="https://github.com/torvalds/linux/blob/master/net/netfilter/nf_conntrack_ftp.c">this</a>
implement connection tracking for individual protocols such that related connections can be found by the
firewall.
</p>
<p>
The syntax in the output of <code class="code">iptables -S</code> 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
</p>
<pre class="code"><code>$ iptables -P INPUT ACCEPT</code></pre>
<p>whereas with</p>
<pre class="code"><code>$ iptables -A INPUT -p tcp --dport 3000 -j ACCEPT</code></pre>
<p>we could append a rule to the INPUT chain to open port 3000 for TCP packets.</p>
<p>
Other useful commands are
</p>
<pre class="code"><code>$ iptables -D &ltchain&gt &ltrulenum&gt</code></pre>
<p>to delete rules or</p>
<pre class="code"><code>$ iptables -R &ltchain&gt &ltrulenum&gt &ltrule-specification&gt</code></pre>
<p>to replace rules with a rule number and a rule specification like</p>
<code class="code">-p &ltproto&gt --dport &ltdestination port&gt -j &lttarget&gt</code>.<br />
<p>You can look up rule numbers with <code class="code">--line-numbers</code>:</p>
<pre class="code"><code>$ iptables -S --line-numbers</code></pre>
<p>These were all commands we will use in this blog post.</p>
<p>
If anything is still unclear, don't hesitate to refer to the
<a target="_blank" href="https://man.archlinux.org/man/iptables.8.en">manual</a>
or ask a question in the <a target="_blank" href="https://stacker.news/items/221471">comments</a>.
</p>
</div>
<hr />
<div class="text-left">
<h1>wireguard</h1>
<h2>installation</h2>
<p>
Follow the instructions <a target="_blank" href="https://www.wireguard.com/install/">here</a> to install WireGuard
on your local and remote machine.<br />
When you're done, you should be able to run the following command:
</p>
<pre class="code"><code>$ wg --version
wireguard-tools v1.0.20210914 - https://git.zx2c4.com/wireguard-tools/</code></pre>
<h2>configuration</h2>
<p>
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.<br />
</p>
<h3>key generation</h3>
<p>
WireGuard uses asymmetric cryptography for its encryption.
Therefore, you need to generate a key pair using the commands
<code class="code">genkey</code> and <code class="code">pubkey</code> on your local
and remote machine. As mentioned in
<a target="_blank" href="https://man.archlinux.org/man/wg.8#pubkey"><code class="code">man wg</code></a>, you can
generate a key pair with secure file
permissions
(handled by <a target="_blank" href="https://wiki.archlinux.org/title/umask"><code class="code">umask</code></a>)
like this:
</p>
<pre class="code"><code>$ umask 077
$ wg genkey | tee /etc/wireguard/wg_private.key | wg pubkey > /etc/wireguard/wg_public.key</code></pre>
<p>
This will generate a private key at <i>/etc/wireguard/wg_private.key</i> and a public key at
<i>/etc/wireguard/wg_public.key</i> which are only readable and writeable by the current user
<a href="#ft-3">[3]</a>.
</p>
<p><i>local machine keys:</i></p>
<pre class="code"><code>$ cat /etc/wireguard/wg_private.key
l0c4l+s3cR37+RDr+dJdgX/ACeRQLANiduQRJK9O23A=
$ cat /etc/wireguard/wg_public.key
/wH4OzafBUJVvRGzK8itUweV/GpwoUzn7OS99lr7gHI=</code></pre>
<p><i>remote machine keys:</i></p>
<pre class="code"><code>$ cat /etc/wireguard/wg_private.key
r3M073+s3cR37+fouaQZbP5QqfgwypHjKGBNmztxNEc=
$ cat /etc/wireguard/wg_public.key
GL33DRrI8/2yAT6+r5mTtBLd7CoErAAsio3yNqQ3K1M=</code></pre>
</p>
<h3>ip range selection</h3>
<p>
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.
<strong>The important part is to not pick an IP range which is already in use.</strong>
Fortunately, the IPv4 specification reserved following IP ranges for use in private networks
<a href="#ft-4">[4]</a>:
<ul>
<li>10.0.0.0/8</li>
<li>172.16.0.0/12</li>
<li>192.168.0.0/16</li>
</ul>
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 <a href="#ft-5">[5]</a>:
<pre class="code"><code>$ 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 <strong>192.168.178.146/24</strong> 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</code></pre>
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 <code class="code">ip address</code> afterwards as above.<br />
In this blog post, we will assume that <strong>10.172.16.0/24</strong> is still free to use and thus can be selected
for our VPN.
</p>
<h3>peer configuration</h3>
<p>
We can configure our peers via the file <i>/etc/wireguard/wg0.conf</i> <a href="#ft-6">[6]</a>.
As with the keys, it makes sense to run <code class="code">umask 077</code> before creating the files.
The files will then be created with read and write access only given to the current user.
</p>
<p>
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 <code>ListenPort</code> 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.
</p>
<p>
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>[Peer]</code> per peer.
Since we only have one peer per peer, there will only be a single <code>[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>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.
</p>
<p>
For the local machine, we will also set <code>Endpoint</code> to the <strong>public</strong>
IP address of the remote machine
and port as used in <code>ListenPort</code>.
This lets WireGuard know how to reach the peer to establish a VPN connection.
</p>
<p>
To keep the connection alive, we will also use
<code>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>PersistentKeepalive</code> to
keep NAT mappings valid.
</p>
<p><i>local machine configuration:</i></p>
<pre class="code"><code>[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</code></pre>
<p><i>remote machine configuration:</i></p>
<pre class="code"><code>[Interface]
Address = 10.172.16.1/32
PrivateKey = r3M073+s3cR37+fouaQZbP5QqfgwypHjKGBNmztxNEc=
ListenPort = 51913
[Peer]
AllowedIPs = 10.172.16.2/32
PublicKey = /wH4OzafBUJVvRGzK8itUweV/GpwoUzn7OS99lr7gHI=</code></pre>
<p>
As a last step, make sure that the file permissions are correctly set and the owner of all created files is root:
</p>
<pre class="code"><code>$ 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</code></pre>
<p>
If the public key is readable by other users, that's fine.
If there is something wrong with your file permissions, run these commands:
</p>
<pre class="code"><code>$ chown root:root -R /etc/wireguard
$ chmod 600 -R /etc/wireguard</code></pre>
<h2>interface control</h2>
<p>
We are now done with all WireGuard configuration.
Run this on the local and remote machine to bring the interfaces up:
</p>
<pre class="code"><code>$ wg-quick up wg0</code></pre>
<p>If you use <code class="code">systemd</code>, you can run
<code class="code">wg-quick up wg0</code> on boot using a systemd service:
</p>
<pre class="code"><code>$ systemctl enable wg-quick@wg0</code></pre>
<p>To bring the interface down, run this:</p>
<pre class="code"><code>$ wg-quick down wg0</code></pre>
<p>To see configuration and peer information of interfaces, run <code class="code">wg</code>:</p>
<pre class="code"><code>$ 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</code></pre>
<p>
As you can see in the line beginning with <code>transfer</code>, we did not receive any packets yet.
This is because we did not properly configure our firewalls yet.
</p>
</div>
<hr />
<div class="text-left">
<h1>firewall configuration with iptables</h1>
<h2>initial configuration</h2>
<p>
We are starting with the following minimal set of firewall rules for the local machine:
</p>
<pre class="code"><code>(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</code></pre>
<p>and remote machine:</p>
<pre class="code"><code>(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</code></pre>
<p>
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
</p>
<pre class="code"><code>(local) $ ping 10.172.16.1</code></pre>
<p>at the local machine and</p>
<pre class="code"><code>(remote) $ ping 10.172.16.2</code></pre>
<p>at the remote machine.</p>
<p>
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.
</p>
<p>
To fully understand which rules are required and why, we will configure the firewall in four steps:
</p>
<ol>
<li>Configure local OUTPUT chain with remote INPUT chain policy set to ACCEPT</li>
<li>Keep VPN connection up with remote INPUT chain policy switched back to DROP</li>
<li>Configure remote OUTPUT chain with local INPUT chain policy set to ACCEPT</li>
<li>Keep VPN connection up with local INPUT chain policy switched back to DROP</li>
</ol>
<p>
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.
</p>
<h2>local OUTPUT chain configuration</h2>
<p>
As mentioned, we will set the INPUT chain policy of the remote filter table to ACCEPT first:
</p>
<pre class="code"><code>(remote) $ iptables -P INPUT ACCEPT</code></pre>
<pre class="code"><code><span class="diff-remove">- -P INPUT DROP</span>
<span class="diff-add">+ -P INPUT ACCEPT</span>
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT</code></pre>
<p>
We know that <code class="code">ping</code> uses ICMP packets so we need to allow ICMP in our local firewall:
</p>
<pre class="code"><code>(local) $ iptables -A OUTPUT -p icmp -j ACCEPT</code></pre>
<p>We also know that WireGuard uses UDP. This mean we need to also allow outgoing UDP packets:</p>
<pre class="code"><code>(local) $ iptables -A OUTPUT -p udp -j ACCEPT</code></pre>
<p>
We have made these changes locally now:
</p>
<pre class="code"><code> -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
<span class="diff-add">+ -A OUTPUT -p icmp -j ACCEPT</span>
<span class="diff-add">+ -A OUTPUT -p udp -j ACCEPT</span></code></pre>
<p>
This is sufficient for a ping from the local to the remote machine:
</p>
<pre class="code"><code>(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</code></pre>
<p>The current rules are very broad however. This is bad for security. We will fix this now.</p>
<p>However, if we limit UDP packets to only the <code>wg0</code> interface, the ping stops working:</p>
<pre class="code"><code>(local) $ iptables -R OUTPUT 3 -o wg0 -p udp -j ACCEPT</code></pre>
<pre class="code"><code> -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
<span class="diff-remove">- -A OUTPUT -p udp -j ACCEPT</span>
<span class="diff-add">+ -A OUTPUT -o wg0 -p udp -j ACCEPT</span></code></pre>
<p>
This is because <code>wg0</code> is the <i>virtual network interface</i>, 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:
</p>
<img class="flex m-auto" src="/blog/img/wireguard_layering.png" />
<p>
If we use the physical network interface
(which is <code>enp3s0</code> for the local machine as can be seen in <code class="code">ip address</code>),
the ping works again:
</p>
<pre class="code"><code>(local) $ iptables -R OUTPUT 3 -o enp3s0 -p udp -j ACCEPT</code></pre>
<pre class="code"><code> -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
<span class="diff-remove">- -A OUTPUT -o wg0 -p udp -j ACCEPT</span>
<span class="diff-add">+ -A OUTPUT -o enp3s0 -p udp -j ACCEPT</span></code></pre>
<p>
We can also limit the UDP packets to port 51913 of our remote machine:
</p>
<pre class="code">
<code>(local) $ iptables -R OUTPUT 3 -o enp3s0 -p udp -d 139.162.153.133 --dport 51913 -j ACCEPT</code></pre>
<pre
class="code"><code> -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
<span class="diff-remove">- -A OUTPUT -o enp3s0 -p udp -j ACCEPT</span>
<span class="diff-add">+ -A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT</span></code></pre>
<p>
What would not work is to limit the UDP packets using internal IPs since the physical network interface is unaware
of our VPN:
</p>
<pre class="code"><code>(local) $ iptables -R OUTPUT 3 -o enp3s0 -p udp -d 10.172.16.1 -j ACCEPT</code></pre>
<pre class="code">
<code> -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
<span class="diff-remove">- -A OUTPUT -o enp3s0 -p udp -j ACCEPT</span>
<span class="diff-add">+ -A OUTPUT -d 10.172.16.1/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT</span></code></pre>
<p>
To confirm our understanding, we can limit the ICMP packets to only the <code>wg0</code> interface. The ping
should continue to work:
</p>
<pre class="code">
<code>(local) $ iptables -R OUTPUT 2 -o wg0 -p icmp -j ACCEPT</code></pre>
<pre class="code">
<code> -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
<span class="diff-remove">- -A OUTPUT -p icmp -j ACCEPT</span>
<span class="diff-add">+ -A OUTPUT -o wg0 -p icmp -j ACCEPT</span>
-A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT</code></pre>
</p>
<p>
And it indeed does:
</p>
<pre class="code"><code>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</code></pre>
<p>
Usually, all traffic is allowed inside a VPN. Therefore, this rule is commonly used:
</p>
<pre class="code">
<code>(local) $ iptables -R OUTPUT 2 -o wg0 -j ACCEPT</code></pre>
<p>
Done. The changes we applied to the local firewall configuration are:
</p>
<pre class="code">
<code> -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
<span class="diff-add">+ -A OUTPUT -o wg0 -j ACCEPT</span>
<span class="diff-add">+ -A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT</span></code></pre>
<h2>switch remote INPUT chain policy back to DROP</h2>
<p>We will set the remote INPUT chain policy back to DROP now.</p>
<pre class="code"><code><span class="diff-remove">- -P INPUT ACCEPT</span>
<span class="diff-add">+ -P INPUT DROP</span>
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT</code></pre>
<p>
The ping stopped working but we know that the
local OUTPUT chain is properly configured.
<p>
After allowing inbound ICMP <i>and</i> UDP packets, the ping from the local machine to the remote machine works
again:
</p>
<pre class="code"><code> -P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
<span class="diff-add">+ -A INPUT -p icmp -j ACCEPT</span>
<span class="diff-add">+ -A INPUT -p udp -j ACCEPT</span>
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
</code></pre>
<p>
We will limit the UDP packets to only port 51913 and the physical network interface.
The physical network interface of the remote machine is <code>eth0</code>:
</p>
<pre class="code"><code>(remote) $ iptables -R INPUT 3 -i eth0 -p udp --dport 51913 -j ACCEPT</code></pre>
<p>
To actually enable all VPN traffic from the local to the remote machine, we also need to allow it on the remote
machine:
</p>
<pre class="code"><code>(remote) $ iptables -R INPUT 2 -i wg0 -j ACCEPT</code></pre>
<p>
Done. We applied following changes to the remote firewall:
</p>
<pre class="code"><code><span class="diff-remove">- -P INPUT ACCEPT</span>
<span class="diff-add">+ -P INPUT DROP</span>
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
<span class="diff-add">+ -A INPUT -i wg0 -j ACCEPT</span>
<span class="diff-add">+ -A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT</span>
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT</code></pre>
<h2>remote OUTPUT chain configuration</h2>
<p>
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).</p>
<p>
To focus on the OUTPUT chain configuration, we will set the local INPUT chain policy to ACCEPT:
</p>
<pre class="code"><code><span class="diff-remove">- -P INPUT DROP</span>
<span class="diff-add">+ -P INPUT ACCEPT</span>
-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</code></pre>
<p>
And start with allowing outgoing ICMP packets:
</p>
<pre class="code"><code> -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
<span class="diff-add">+ -A OUTPUT -p icmp -j ACCEPT</span></code></pre>
<p>
However, this time, we notice that the ping already works even without allowing UDP packets:
</p>
<pre class="code"><code>(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</code></pre>
<p>
The explanation is that the UDP packets are able to use an established connection.
Only allowing TCP packets kills the connection <a href="#ft-7">[7]</a>:
</p>
<pre class="code"><code> -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
<span class="diff-remove">- -A OUTPUT -m state --state ESTABLISHED -j ACCEPT</span>
<span class="diff-add">+ -A OUTPUT -p tcp -m state --state ESTABLISHED -j ACCEPT</span>
-A OUTPUT -p icmp -j ACCEPT</code></pre>
<p>
We could allow UDP packets through a separate rule ... :
</p>
<pre class="code"><code> -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</span>
-A OUTPUT -p icmp -j ACCEPT
<span class="diff-add">+ -A OUTPUT -p udp -j ACCEPT</span></code></pre>
<p>
... 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:
</p>
<pre class="code"><code>(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</code></pre>
<p>
Done. We effectively only added a single rule to the remote firewall:
</p>
<pre class="code"><code> -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
<span class="diff-add">+ -A OUTPUT -o wg0 -j ACCEPT</span></code></pre>
<h2>switch local INPUT policy back to DROP</h2>
<p>
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.
</p>
<p>When going back to DROP as the INPUT chain policy ... :</p>
<pre class="code"><code><span class="diff-remove">- -P INPUT ACCEPT</span>
<span class="diff-add">+ -P INPUT DROP</span>
-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</code></pre>
<p>... we notice that the ping continues to work. This is because of the first INPUT rule:</p>
<pre class="code"><code>-A INPUT -m state --state ESTABLISHED -j ACCEPT</code></pre>
<p>
If we kill the connection and then run <code class="code">ping</code> again, it no longer works:
<pre class="code"><code>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.
</code></pre>
<p>This is expected since it only worked because of an established connection.</p>
<p>After allowing ICMP traffic, the ping also works immediately again:</p>
<pre class="code"><code> -P INPUT DROP</span>
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
<span class="diff-add">+ -A INPUT -p icmp -j ACCEPT</span>
-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</code></pre>
<p>
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.</p>
<p>
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:
</p>
<pre class="code"><code> -P INPUT DROP</span>
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
-A INPUT -p icmp -j ACCEPT
<span class="diff-remove">- -A INPUT -p icmp -j ACCEPT</span>
<span class="diff-add">+ -A INPUT -i wg0 -j ACCEPT</span>
<span class="diff-add">+ -A INPUT -s 139.162.153.133/32 -i enp3s0 -p udp -m udp --sport 51913 -j ACCEPT</span>
-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</code></pre>
<h2>final configuration</h2>
<p><i>local firewall configuration:</i></p>
<pre
class="code"><code> -P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -m state --state ESTABLISHED -j ACCEPT
<span class="diff-add">+ -A INPUT -i wg0 -j ACCEPT</span>
<span class="diff-add">+ -A INPUT -s 139.162.153.133 -i enp3s0 -p udp -m udp --sport 51913 -j ACCEPT</span>
-A OUTPUT -d 139.162.153.133/32 -p tcp -m tcp --dport 22 -j ACCEPT
<span class="diff-add">+ -A OUTPUT -o wg0 -j ACCEPT</span>
<span class="diff-add">+ -A OUTPUT -d 139.162.153.133/32 -o enp3s0 -p udp -m udp --dport 51913 -j ACCEPT</span></code></pre>
<p><i>remote firewall configuration:</i></p>
<pre class="code"><code> -P INPUT DROP
-P FORWARD DROP
-P OUTPUT DROP
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
<span class="diff-add">+ -A INPUT -i wg0 -j ACCEPT</span>
<span class="diff-add">+ -A INPUT -i eth0 -p udp -m udp --dport 51913 -j ACCEPT</span>
-A OUTPUT -m state --state ESTABLISHED -j ACCEPT
<span class="diff-add">+ -A OUTPUT -o wg0 -j ACCEPT</span></code></pre>
</div>
<hr />
<div>
<p>
Thanks for reading my first blog post!
If you want to read more content like this, please consider subscribing via
<a href="/blog/rss.xml">RSS</a>.
</p>
<p>
Also, I would highly appreciate any feedback in the
<a target="_blank" href="https://stacker.news/items/221471">comments</a>.
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 <i>really</i>
need <strong>any</strong> kind of feedback.
I'll even pay you 100 sats!
</p>
</div>
<hr />
<div class="text-left">
<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 <a target="_blank"
href="https://wiki.archlinux.org/title/iptables#Basic_concepts">here</a>.
</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 <a target="_blank" href="https://wiki.archlinux.org/title/umask">here</a>,
<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
<a target="_blank" href="https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses">wikipedia
article</a>
about private networks
</span><br />
<span id="ft-5">[5]
If you are confused by the /24 notation, you can read about CIDR
<a target="_blank" href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing">here</a>.
</span><br />
<span id="ft-6">[6] You could use any other path, but <i>/etc/wireguard/</i> is searched automatically by
<code class="code">wg-quick</code> 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>
</div>
</div>

View File

@ -12,6 +12,7 @@
<script defer data-api="/api/event" data-domain="ekzyis.com" src="/js/script.js"></script>
{{ if eq .Env "development" }}
<script>
const scroll = (y) => window.scrollTo(0, 925*y)
async function hotReload() {
console.log("running in development mode")
const r = await fetch("/hot-reload")

View File

@ -8,7 +8,7 @@
{{ template "nav.html" }}
</header>
<div class="container flex flex-column">
{{ .Body }}
{{ .Content }}
</div>
{{ template "footer.html" . }}
</body>

View File

@ -37,6 +37,7 @@ h3 {
}
hr {
width: 100%;
border: none;
height: 1px;
background: repeating-linear-gradient(90deg,#fff,#fff 6px,transparent 1px,transparent 8px);
@ -131,4 +132,8 @@ pre {
}
.pb-1 {
padding-bottom: 1em;
}
}
.bg-transparent {
background-color: transparent !important;
}

View File

@ -12,6 +12,6 @@ function cleanup() {
trap cleanup EXIT
sync
while inotifywait -r -e modify html/; do
while inotifywait -r -e modify html/ blog/ *.go; do
sync
done

26
syntax.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"fmt"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
)
func SyntaxHighlighting(element *goquery.Selection) {
if element.HasClass("language-diff") {
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"))
}
}