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:
parent
8c6ef67442
commit
bf88488586
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
html/pages/blog
|
||||
public/**/*.html
|
||||
hot-reload
|
||||
|
179
blog.go
Normal file
179
blog.go
Normal 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)
|
||||
}
|
||||
}
|
921
blog/20230809-demystifying-wireguard-and-iptables.md
Normal file
921
blog/20230809-demystifying-wireguard-and-iptables.md
Normal 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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:
|
||||
|
||||

|
||||
|
||||
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>
|
@ -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
108
build.go
@ -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
8
go.mod
@ -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
47
go.sum
@ -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
104
html.go
Normal 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
|
||||
}
|
@ -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 <chain> <rulenum></code></pre>
|
||||
<p>to delete rules or</p>
|
||||
<pre class="code"><code>$ iptables -R <chain> <rulenum> <rule-specification></code></pre>
|
||||
<p>to replace rules with a rule number and a rule specification like</p>
|
||||
<code class="code">-p <proto> --dport <destination port> -j <target></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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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>
|
@ -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")
|
||||
|
@ -8,7 +8,7 @@
|
||||
{{ template "nav.html" }}
|
||||
</header>
|
||||
<div class="container flex flex-column">
|
||||
{{ .Body }}
|
||||
{{ .Content }}
|
||||
</div>
|
||||
{{ template "footer.html" . }}
|
||||
</body>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
26
syntax.go
Normal 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"))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user