## 1. Project Overview & Quickstart (xjasonlyu/tun2socks)
[
[
[
[
[
[
[
## Features
- **Universal Proxying**: Transparently routes all network traffic from any application through a proxy.
- **Multi-Protocol**: Supports HTTP/SOCKS/Shadowsocks/SSH/Relay proxies with optional authentication.
- **Cross-Platform**: Runs on Linux/macOS/Windows/FreeBSD/OpenBSD with platform-specific optimizations.
- **Gateway Mode**: Acts as a Layer 3 gateway to route traffic from other devices on the same network.
- **Full IPv6 Compatibility**: Natively supports IPv6; seamlessly tunnels IPv4 over IPv6 and vice versa.
- **User-Space Networking**: Leverages the **[gVisor](https://github.com/google/gvisor)** network stack for enhanced
performance and flexibility.
## Benchmarks
For all scenarios of usage, tun2socks performs best.
See [benchmarks](https://github.com/xjasonlyu/tun2socks/wiki/Benchmarks) for more details.
## Documentation
- [Install from Source](https://github.com/xjasonlyu/tun2socks/wiki/Install-from-Source)
- [Quickstart Examples](https://github.com/xjasonlyu/tun2socks/wiki/Examples)
- [Memory Optimization](https://github.com/xjasonlyu/tun2socks/wiki/Memory-Optimization)
Full documentation and technical guides can be found at [Wiki](https://github.com/xjasonlyu/tun2socks/wiki).
## Community
Welcome and feel free to ask any questions at [Discussions](https://github.com/xjasonlyu/tun2socks/discussions).
## Credits
- [google/gvisor](https://github.com/google/gvisor) - Application Kernel for Containers
- [wireguard-go](https://git.zx2c4.com/wireguard-go) - Go Implementation of WireGuard
- [wintun](https://git.zx2c4.com/wintun/) - Layer 3 TUN Driver for Windows
## License
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fxjasonlyu%2Ftun2socks?ref=badge_large)
## Star History
[1]: https://img.shields.io/github/actions/workflow/status/xjasonlyu/tun2socks/docker.yml?branch=main&logo=github
[2]: https://img.shields.io/github/actions/workflow/status/xjasonlyu/tun2socks/linter.yml?branch=main&logo=githubactions&label=golangci-lint
[3]: https://img.shields.io/github/go-mod/go-version/xjasonlyu/tun2socks?logo=go
[4]: https://qlty.sh/gh/xjasonlyu/projects/tun2socks/maintainability.svg
[5]: https://img.shields.io/github/license/xjasonlyu/tun2socks
[6]: https://img.shields.io/docker/pulls/xjasonlyu/tun2socks?logo=docker
[7]: https://img.shields.io/github/v/release/xjasonlyu/tun2socks?logo=smartthings
## 2. Official Technical Reference & Guides (xjasonlyu/xjasonlyu.github.io)
## File: README.md
# Hi, I'm Jason Lyu
> Website inspired by [skyzh-site](https://github.com/skyzh/skyzh-site).
This repo contains the source code of my personal website and is powered by [Cloudflare Pages](https://pages.cloudflare.com/).
---
## File: content/posts/2025-10-26-ipv6-proxy-pool/index.md
---
title: "IPv6 Proxy Pool"
date: 2025-10-26T14:27:28-04:00
Tags: [linux, proxy, ipv6, proxy pool, network]
Categories: [Proxy, IPv6, Network, Linux, OS]
DisableComments: false
toc: true
---
This article is a step-by-step guide (and personal note) on how to configure an IPv6 proxy pool.
It's based on practical experiments with Debian/Ubuntu systems, with special thanks to [zu1k](https://github.com/zu1k) and [missuo](https://github.com/missuo) for their excellent base guides and references.
## How It Works
**Assumptions:**
- We have a dedicated IPv6 subnet assigned by the provider - typically `/64` (a larger prefix such as `/54` also works).
- The operating system is **Debian** or **Ubuntu**.
### Route and Bind
Once we have the IPv6 subnet, we need to add a route for it.
Obviously, we don't want to add every address in this `/64` subnet individually - that would quickly exhaust system resources. Instead, we can use the `ip route add local` trick to tell the kernel that all addresses in `2606:4700:4700::/64` are locally "owned" by the `ens3` interface.
> The `dev ens3` doesn't literally assign ownership of all these addresses to the interface; it just specifies the device used for routing lookups. Packets to that prefix will be delivered locally regardless of whether any individual address is configured.
```sh
ip route add local 2606:4700:4700::/64 dev ens3
```
To verify that the route has been added successfully, run:
```sh
ip -6 route show table local
```
Next, we need to enable the `ip_nonlocal_bind` option in the Linux kernel. This allows processes to bind to IP addresses that are not explicitly assigned to a local interface. Otherwise, any attempt to bind to such addresses will fail.
```sh
sysctl net.ipv6.ip_nonlocal_bind=1
```
### Configure NDP
If the VPS shares the same Layer 2 (VLAN) network with other hosts, IPv6 Neighbor Discovery (ND) is used to resolve MAC addresses (similar to ARP in IPv4).
Because we didn't explicitly assign every address in the `/64` subnet to the interface, the kernel won't respond to Neighbor Solicitation messages for those unassigned addresses.
To handle these requests, we can use an Neighbor Discovery Proxy (NDP) to reply on behalf of the kernel.
> However, if the VPS is **directly routed** - meaning the upstream router already knows your MAC address and sends traffic for the entire prefix straight to you - then no NDP proxy is required.
>
> We can check whether an NDP proxy is needed by capturing Neighbor Solicitation (ICMPv6 type 135) messages:
>
> ```sh
> tcpdump -i icmp6 and 'ip6[40] == 135'
> ```
>
> If we see such packets coming from the upstream router, we'll need to run an NDP proxy.
> If not, we can simply assign the IPv6 subnet to `dev lo`, since the prefix is directly routed to our host.
To set up NDP proxying, install ndppd:
```sh
apt install ndppd
```
Then edit `/etc/ndppd.conf` (replace the interface name `ens3` and the prefix with actual values):
```text
route-ttl 30000
proxy ens3 {
router no
timeout 500
ttl 30000
rule 2606:4700:4700::/64 {
static
}
}
```
Finally, enable and restart the service:
```sh
systemctl enable ndppd.service
systemctl restart ndppd.service
```
> **NOTE**: If your setup relies on NDP, avoid using subnets larger than `/120`. Otherwise, the system may generate too many neighbor entries, which can flood the ND table and cause performance issues.
### Verify It Works
To confirm that the setup is functional, we can use `curl --interface` to bind outbound connections to specific IPv6 addresses within our `/64` subnet.
If everything is configured correctly, each request should appear to come from the specified address.
```sh
$ curl --interface 2606:4700:4700::a ipv6.ip.sb
2606:4700:4700::a
$ curl --interface 2606:4700:4700::b ipv6.ip.sb
2606:4700:4700::b
```
## Docker Setup
Personally, I don't like exposing or hosting services directly on my host machine, so I use Docker for a bit of isolation and easier management.
When it comes to making an IPv6 proxy pool inside Docker, the main challenge is networking - specifically, how containers get their IPv6 connectivity (for a broad range of IPv6 addresses).
Docker supports several main network modes:
- bridge
- host
- none
- macvlan
- ipvlan
The bridge network doesn't work for our use case. It creates a NATed environment behind the host network, so no matter what IPv6 address you assign inside the container, the actual outbound IP on the public internet will still be your host's IP.
You _could_ mess around with host routing rules to make it work, but that's unnecessary and goes against the whole purpose of this setup.
The none network - well, it's literally "no network", so that's not what we want either.
MacVLAN is actually a great fit for this kind of setup. However, most VPS or IDC providers don't allow creating new MAC addresses in their local network. Doing so might trigger security alerts or even violate their terms of service. If you're running this in your own physical network like a HomeLab, though, that's perfectly fine - MacVLAN would be the best and cleanest option AFAIK.
That leaves us with two remaining options: host and ipvlan. Both can work, but each has its own trade-offs.
### Host
Using the **host** network mode is basically the same as setting everything up directly on the host itself.
With the following Docker Compose setup, you can enable the host network and allow non-local IPv6 bindings via `sysctls`:
```yaml
services:
proxy-pool:
cap_add:
- NET_ADMIN
sysctls:
net.ipv6.ip_nonlocal_bind: "1"
network_mode: host
...
```
You might need a small custom or entrypoint script to set up the ip route add local rule before starting the actual proxy process.
The main drawback of using the host mode is that it’s not fully isolated - any route changes made inside the container will directly affect the host's network namespace.
So personally, I don't really see the point of running it in host mode unless you have other specific needs. If you're going to do that, you might as well just run it directly on the host.
### IPVlan
I initially thought **IPvlan** could be the ultimate solution - it
- shares the same MAC address as the parent interface on the host, and
- provides proper network isolation between containers and the host.
However, things didn't go as smoothly as I expected.
After setting everything up with the IPvlan network (including enabling non-local bind and the ip route local trick),
I noticed something odd: I still couldn't send requests using IP addresses that weren't explicitly assigned to the interface.
So I traced the issue further using `tcpdump`. Outgoing packets were sent successfully, and reply packets were indeed routed back. I could even see _Neighbor Solicitation_ (NS) messages on the network asking for the IP's MAC address - and I could confirm those packets reached the host interface. But they never made it into the container’s network namespace.
So I started digging into the IPvlan driver implementation.
#### How IPvlan Resolves Incoming Packets
Inside the kernel source, there's a function called `ipvlan_addr_lookup` which looks up the associated port (container) for a given IP address when handling a packet via `ipvlan_handle_mode_l3`.
```c
struct ipvl_addr *ipvlan_addr_lookup(struct ipvl_port *port, void *lyr3h,
int addr_type, bool use_dest)
{
struct ipvl_addr *addr = NULL;
switch (addr_type) {
#if IS_ENABLED(CONFIG_IPV6)
case IPVL_IPV6: {
struct ipv6hdr *ip6h;
struct in6_addr *i6addr;
ip6h = (struct ipv6hdr *)lyr3h;
i6addr = use_dest ? &ip6h->daddr : &ip6h->saddr;
addr = ipvlan_ht_addr_lookup(port, i6addr, true);
break;
}
```
This lookup uses a hash table, implemented as follows:
```c
static struct ipvl_addr *ipvlan_ht_addr_lookup(const struct ipvl_port *port,
const void *iaddr, bool is_v6)
{
struct ipvl_addr *addr;
u8 hash;
hash = is_v6 ? ipvlan_get_v6_hash(iaddr) :
ipvlan_get_v4_hash(iaddr);
hlist_for_each_entry_rcu(addr, &port->hlhead[hash], hlnode)
if (addr_equal(is_v6, addr, iaddr))
return addr;
return NULL;
}
```
#### When Does This Hash Table Get Populated
It's populated either when the IPvlan device is opened, or when an address is added to the interface.
When opened:
```c
static int ipvlan_open(struct net_device *dev)
{
struct ipvl_dev *ipvlan = netdev_priv(dev);
struct ipvl_addr *addr;
if (ipvlan->port->mode == IPVLAN_MODE_L3 ||
ipvlan->port->mode == IPVLAN_MODE_L3S)
dev->flags |= IFF_NOARP;
else
dev->flags &= ~IFF_NOARP;
rcu_read_lock();
list_for_each_entry_rcu(addr, &ipvlan->addrs, anode)
ipvlan_ht_addr_add(ipvlan, addr);
rcu_read_unlock();
return 0;
}
```
When an IPv6 address is added or removed:
```c
static int ipvlan_addr6_event(struct notifier_block *unused,
unsigned long event, void *ptr)
{
struct inet6_ifaddr *if6 = (struct inet6_ifaddr *)ptr;
struct net_device *dev = (struct net_device *)if6->idev->dev;
struct ipvl_dev *ipvlan = netdev_priv(dev);
if (!ipvlan_is_valid_dev(dev))
return NOTIFY_DONE;
switch (event) {
case NETDEV_UP:
if (ipvlan_add_addr6(ipvlan, &if6->addr))
return NOTIFY_BAD;
break;
case NETDEV_DOWN:
ipvlan_del_addr6(ipvlan, &if6->addr);
break;
}
return NOTIFY_OK;
}
```
#### The Dead End
And here's the problem:
Only IPs that are explicitly assigned to the IPvlan interface get inserted into that hash table. When a packet arrives, the kernel looks it up there - if it doesn't exist, it drops or ignores it.
Even if we tried to fake-add addresses manually, maintaining that table for an entire `/64` network would be unmanageable. A `/64` prefix has 18 quintillion addresses, which is far beyond what's feasible for IPvlan's current implementation.
Sure, in theory we could modify the kernel driver, recompile it, or hook something with eBPF to bypass that lookup - but the added complexity just isn't worth it for this use case.
## Conclusion
TBH, I don't have anything to conclude here. 💁
## References
-
-
-
-
---
## File: content/posts/2023-06-27-github-gpg-keys/index.md
---
title: "Github GPG Keys"
date: 2023-06-27T17:31:27+08:00
Tags: [github, gpg, git, signature, signingkey]
Categories: [GitHub, GPG, Security, Git]
DisableComments: false
toc: false
---
## GitHub Web-flow GPG Key
GitHub sets the committer for all commits made using their web interface to the user [web-flow](https://github.com/web-flow).
For any given GitHub account, you can add `.gpg` to its URL to get its public key — so for web-flow, you can find it at :
```text
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsBNBFmUaEEBCACzXTDt6ZnyaVtueZASBzgnAmK13q9Urgch+sKYeIhdymjuMQta
x15OklctmrZtqre5kwPUosG3/B2/ikuPYElcHgGPL4uL5Em6S5C/oozfkYzhwRrT
SQzvYjsE4I34To4UdE9KA97wrQjGoz2Bx72WDLyWwctD3DKQtYeHXswXXtXwKfjQ
7Fy4+Bf5IPh76dA8NJ6UtjjLIDlKqdxLW4atHe6xWFaJ+XdLUtsAroZcXBeWDCPa
buXCDscJcLJRKZVc62gOZXXtPfoHqvUPp3nuLA4YjH9bphbrMWMf810Wxz9JTd3v
yWgGqNY0zbBqeZoGv+TuExlRHT8ASGFS9SVDABEBAAHNNUdpdEh1YiAod2ViLWZs
b3cgY29tbWl0IHNpZ25pbmcpIDxub3JlcGx5QGdpdGh1Yi5jb20+wsBiBBMBCAAW
BQJZlGhBCRBK7hj4Ov3rIwIbAwIZAQAAmQEIACATWFmi2oxlBh3wAsySNCNV4IPf
DDMeh6j80WT7cgoX7V7xqJOxrfrqPEthQ3hgHIm7b5MPQlUr2q+UPL22t/I+ESF6
9b0QWLFSMJbMSk+BXkvSjH9q8jAO0986/pShPV5DU2sMxnx4LfLfHNhTzjXKokws
+8ptJ8uhMNIDXfXuzkZHIxoXk3rNcjDN5c5X+sK8UBRH092BIJWCOfaQt7v7wig5
4Ra28pM9GbHKXVNxmdLpCFyzvyMuCmINYYADsC848QQFFwnd4EQnupo6QvhEVx1O
j7wDwvuH5dCrLuLwtwXaQh0onG4583p0LGms2Mf5F+Ick6o/4peOlBoZz48=
=HXDP
-----END PGP PUBLIC KEY BLOCK-----
```
You can then import and trust that public key.
As shown in this thread:
```sh
$ curl https://github.com/web-flow.gpg | gpg --import
$ gpg --edit-key noreply@github.com
gpg> trust
gpg> save
$ gpg --lsign-key noreply@github.com
```
Ref:
## GitHub SSH Key Fingerprints
Ref:
## Transfer GPG Keys
We will need to transfer the keys. Mac and Linux work the same, storing the keys in `~/.gnupg`.
---
## File: content/posts/2023-04-18-hugo-giscus/index.md
---
title: "How to Enable Giscus Comments System in Hugo"
date: 2023-04-18T18:11:48+08:00
Tags: [hugo, theme, github, comments, giscus]
Categories: [Hugo, Blog, GitHub]
DisableComments: false
toc: true
---
## Introduction to Giscus
[Giscus](https://giscus.app/) is an open-source comment system powered by GitHub Discussions, and it works really well!
- [Open source](https://github.com/giscus/giscus).
- No tracking, no ads, always free.
- No database needed. All data is stored in GitHub Discussions.
- Supports [custom themes](https://github.com/giscus/giscus/blob/main/ADVANCED-USAGE.md#data-theme)!
- Supports [multiple languages](https://github.com/giscus/giscus/blob/main/CONTRIBUTING.md#adding-localizations).
- [Extensively configurable](https://github.com/giscus/giscus/blob/main/ADVANCED-USAGE.md).
- Automatically fetches new comments and edits from GitHub.
- [Can be self-hosted](https://github.com/giscus/giscus/blob/main/SELF-HOSTING.md)!
However, not every Hugo theme supports this comment system. In fact, most of them don't. 🫠
So, in this post, I'll use a somewhat hacky approach (without modifying the theme's source code) to add Giscus support to a theme that doesn't support it out of the box.
> Note: this tutorial uses the [Anatole](https://github.com/lxndrblz/anatole) theme I'm currently using as an example. Other themes will be slightly different, but the basic idea is the same.
## How the Hack Works
First, most themes already support `Disqus` or another comment system. They usually decide whether to enable comments with logic like this:
```html
{{- if .Site.DisqusShortname -}}
{{ i18n "comments" }}
{{ template "_internal/disqus.html" . }}
{{- end -}}
{{- if .Site.Params.utterances.repo -}}
{{ i18n "comments" }}
{{ partial "comments/utterances.html" . }}
{{- end -}}
```
As we can see, the theme enables the corresponding comment system by checking whether its configuration exists in the config file.
That makes things much easier: we can use directories such as `layouts/partials` from [Hugo Templates](https://gohugo.io/templates/base/) to override the original comments code without modifying the theme's source code.
## Add the Giscus HTML Template
Here, we'll replace the `utterances` comment system with `giscus` as an example. Simply create an `utterances.html` file under the local `layouts/partials/comments` directory, and it will override the corresponding source file.
Add the following code to the file:
```html
```
## Update the Config File
To enable Giscus support, we still need to configure our hacked comment system in the config file. Think of it as an activation switch:
```yaml
utterances:
repo: anything here
```
And if nothing unexpected happens, that's it!
## Switch Themes Automatically
Giscus is now configured, but if the theme already supports `Light/Dark` modes, you'll notice that the Giscus theme doesn't change along with the site's color scheme. ~~This is unacceptable.~~
Luckily, we can fix this with some JavaScript. For example, replacing the original code with the following makes Giscus adapt to the current theme when the page loads.
```html
```
But then there's another problem: when we switch the site theme manually, the Giscus theme still doesn't update with it.
So, we need to listen for clicks on the theme switcher and update the Giscus theme accordingly. Here's the final code:
```html
```
Done and dusted! 🎉
Ref: [https://github.com/giscus/giscus/issues/336](https://github.com/giscus/giscus/issues/336)
---
## File: content/posts/2023-04-17-hugo-shortcodes/index.md
---
title: "Collection of Hugo Shortcodes"
date: 2023-04-17T14:24:47+08:00
Tags: [hugo, theme, github, gist, shortcodes]
Categories: [Hugo, Blog, GitHub]
DisableComments: false
toc: true
---
Here are a few handy tricks for Hugo, along with some practical improvements.
## Gist
Hugo can embed Gist code blocks directly using its built-in `shortcodes`.
### Display All Gist Files
```text
{{* gist jmooring 50a7482715eac222e230d1e64dd9a89b */>}}
```
{{< gist jmooring 50a7482715eac222e230d1e64dd9a89b >}}
### Display a Specific File
```text
{{* gist jmooring 50a7482715eac222e230d1e64dd9a89b 1-template.html */>}}
```
{{< gist jmooring 50a7482715eac222e230d1e64dd9a89b 1-template.html >}}
Ref: [Hugo Shortcodes](https://gohugo.io/content-management/shortcodes/)
## GitHub
Hugo itself doesn't support displaying code files from GitHub directly, so we'll need to add a custom shortcode.
### Add the Shortcode
> The old version no longer works in Hugo v0.164.0 because the deprecated global `getJSON` function has been removed. The replacement is still quite simple: fetch the GitHub API response with `resources.GetRemote`, parse it with `transform.Unmarshal`, then decode and highlight the file content using the namespaced functions.
Create a `github.html` file under `layouts/shortcodes`, then paste in the following code:
```text
{{ with resources.GetRemote (printf "https://api.github.com/repos/%s/contents/%s" (.Get "repo") (.Get "file")) }}
{{ $data := . | transform.Unmarshal }}
{{ transform.Highlight ($data.content | encoding.Base64Decode) ($.Get "lang") ($.Get "options") }}
{{ end }}
```
### Display GitHub Code
Simply call the shortcode like this:
```text
{{* github repo="username/repo-name" file="/path/to/file" lang="language" options="highlight-options" */>}}
```
---
## File: content/posts/2022-02-14-hugo-rawhtml/index.md
---
title: "How to Embed Raw Html Code in Hugo"
date: 2022-02-14T14:15:50+08:00
Tags: [HTML, hugo, rawhtml]
Categories: [Hugo, Blog]
DisableComments: false
aliases: [rawhtml]
---
Sometimes Markdown's syntax and features are a bit limiting, and we need to add some extra components. In that case, we can simply embed the HTML directly, like this:
```html
```
Let's test how embedded HTML works in Hugo:
Ref: [https://anaulin.org/blog/hugo-raw-html-shortcode/](https://anaulin.org/blog/hugo-raw-html-shortcode/)
---
## File: content/posts/2021-02-12-update-esxi/index.md
---
title: "How to Update VMware ESXi"
date: 2021-02-12T13:14:53+08:00
Tags: [vmware, esxi, virtualization]
Categories: [VMware, ESXi, OS]
DisableComments: false
aliases: [update-esxi]
---
### VMware ESXi Update References
-
-
### Community Networking Driver for ESXi
-
---
## File: content/posts/2020-12-03-so_bindtodevice/index.md
---
title: "SO_BINDTODEVICE Socket Option in Linux"
date: 2020-12-03T16:14:24+08:00
Tags: [linux, bindtodevice, socketopt, vpn]
Categories: [Linux, Network, OS]
DisableComments: false
toc: true
aliases: [so_bindtodevice]
---
### Motivation
While working on tun2socks, I spent some time on and off looking into how to make socket traffic leave through a specific interface instead of following the routing table. This would prevent certain traffic from going through the TUN interface and getting stuck in an infinite loop.
### Policy Routing + bind
There are actually quite a few ways to do this on Linux. One option is policy routing: use `bind` to bind to a specific interface address, then configure two routing tables based on the source address.
~~I have to complain about Windows here: it supports neither binding to an interface nor policy routing. Maybe I just haven't found the right way yet, but for now I have no idea how to solve this there.~~
The `bind` function only makes the bound interface address the source address of outgoing packets. Route lookup still works exactly as before (it only looks at the destination address), and the reply might not even make it back if the source address is wrong.
### SO_BINDTODEVICE
Of course, Linux has a better solution: `SO_BINDTODEVICE`, which is different from `bind`.
According to the documentation:
> Bind this socket to a particular device like "eth0", as specified in the passed interface name. If the name is an empty string or the option length is zero, the socket device binding is removed. The passed option is a variable-length null-terminated interface name string with the maximum size of IFNAMSIZ. If a socket is bound to an interface, only packets received from that particular interface are processed by the socket. Note that this only works for some socket types, particularly AF_INET sockets. It is not supported for packet sockets (use normal bind(2) there).
> Before Linux 3.8, this socket option could be set, but could not retrieved with getsockopt(2). Since Linux 3.8, it is readable. The optlen argument should contain the buffer size available to receive the device name and is recommended to be IFNAMSZ bytes. The real device name length is reported back in the optlen argument.
Hmm. I sort of understood it, but couldn't find anything about routing.
After digging around some more, I finally found this:
> The bind() system call is frequently misunderstood. It is used to bind to a particular IP address. Only packets destined to that IP address will be received, and any transmitted packets will carry that IP address as their source. bind() does not control anything about the routing of transmitted packets. So for example, if you bound to the IP address of eth0 but you send a packet to a destination where the kernel's best route goes out eth1, it will happily send the packet out eth1 with the source IP address of eth0. This is perfectly valid for TCP/IP, where packets can traverse unrelated networks on their way to the destination.
`SO_BINDTODEVICE` ensures that all traffic from the socket goes through that interface. The routing table is still consulted, but routes for other interfaces are skipped; if no matching route can be found, the result is `network unreachable`.
There is also a similar option called `SO_DONTROUTE`. Despite the name, it doesn't mean "don't route," and the routing table is still consulted. It only avoids gateways, so it works within link scope.
This means we can configure two default routes with different priorities and use different routes after binding to an interface. The same idea can also be used with multiple ports for load balancing.
Also, remember to disable the `rp_filter` kernel parameter. The Linux kernel disables it by default, but some distributions enable it for security. Otherwise, packet captures will show that the data is there, but the socket won't receive it.
```sh
sysctl -w net.ipv4.conf.eth0.rp_filter=0
```
Finally, I verified this with a dual-interface container running in Docker. I'll skip the details here.
### Update (2021-08-27)
There are other ways to solve this besides `BINDTODEVICE`:
- [cgroup + namespace](https://www.wireguard.com/netns/)
- Hack the kernel (pass through the original interface directly)
### References
- [An In-Depth Analysis of SO_DONTROUTE and SO_BINDTODEVICE][1]
- [SO_BINDTODEVICE socket option for Linux 2.0.30+][2]
- [Code Snippet: SO_BINDTODEVICE][3]
[1]: https://blog.51cto.com/dog250/1271769
[2]: http://ftp.slackware-brasil.com.br/slackware-3.5/docs/linux-2.0.34/networking/so_bindtodevice.txt
[3]: https://codingrelic.geekhold.com/2009/10/code-snippet-sobindtodevice.html
---
## File: content/posts/2020-12-03-schoolworks/index.md
---
title: "Undergraduate Schoolworks"
date: 2020-12-03T11:45:50+08:00
hidden: true
Tags: [schoolworks, assignments, projects]
Categories: [Undergraduate]
DisableComments: false
aliases: [schoolworks]
---
## Main
整理了一下,把本科阶段Programming相关的项目作业整合到了GitHub仓库里。
- 👉 [Schoolworks](https://github.com/xjasonlyu/Schoolworks)
因为时间和能力有限,这些项目作业必有错误而且应该不会改了,权当备份了🌚。
究其原因还是自己太菜了,所以还要好好学啊👀。
## Credit
- 👏 [oyiadin](https://github.com/oyiadin/Schoolworks)
---
## File: content/_index.md
---
title: Homepage
---
## Hey, I'm Jason Lyu
Previously CAS MEng @ [McMaster](https://www.mcmaster.ca/)
- [About Me](/about/)
- [Curriculum Vitae](/cv/)
- [Blog](/posts/)
You can find me on:
- Email: `xjasonlyu at gmail dot com`
- [GitHub](https://github.com/xjasonlyu)
- [LinkedIn](https://www.linkedin.com/feed/)
### FAQ
[TBA]