tun2socks Documentation

Tun2socks - powered by gVisor TCP/IP stack

5,184 stars Go Go 1 file ยท ~7,645 tokens #go#golang#gvisor#http-proxy#nat#proxifier#proxy#shadowsocks
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy tun2socks on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name tun2socks --restart always xjasonlyu/tun2socks:latest

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 network stack for enhanced
    performance and flexibility.

Benchmarks

For all scenarios of usage, tun2socks performs best.
See benchmarks for more details.

Documentation

Full documentation and technical guides can be found at Wiki.

Community

Welcome and feel free to ask any questions at Discussions.

Credits

License

Star History

2. Official Technical Reference & Guides (xjasonlyu/xjasonlyu.github.io)

File: README.md

Hi, I'm Jason Lyu

Website inspired by skyzh-site.

This repo contains the source code of my personal website and is powered by Cloudflare Pages.


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 and 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 <eth0> 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.

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 https://github.com/web-flow.gpg:

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 [email protected]
gpg> trust
gpg> save
$ gpg --lsign-key [email protected]

Ref: https://stackoverflow.com/questions/60482588/what-is-githubs-public-gpg-key

GitHub SSH Key Fingerprints

Ref: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints

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 is an open-source comment system powered by GitHub Discussions, and it works really well!

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 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 -}}

    <h2>{{ i18n "comments" }}</h2>
    {{ template "_internal/disqus.html" . }}

{{- end -}}
{{- if .Site.Params.utterances.repo -}}

    <h2>{{ i18n "comments" }}</h2>
    {{ 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 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
<script 
    src="https://giscus.app/client.js"
    data-repo="[ENTER REPO HERE]"
    data-repo-id="[ENTER REPO ID HERE]"
    data-category="[ENTER CATEGORY NAME HERE]"
    data-category-id="[ENTER CATEGORY ID HERE]"
    data-mapping="pathname"
    data-strict="0"
    data-reactions-enabled="1"
    data-emit-metadata="0"
    data-input-position="bottom"
    data-theme="preferred_color_scheme"
    data-lang="en"
    crossorigin="anonymous"
    async
></script>

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
<script>
    let giscusTheme = localStorage.getItem("theme");
    let giscusAttributes = {
        "src": "https://giscus.app/client.js",
        "data-repo": "[ENTER REPO HERE]",
        "data-repo-id": "[ENTER REPO ID HERE]",
        "data-category": "[ENTER CATEGORY NAME HERE]",
        "data-category-id": "[ENTER CATEGORY ID HERE]",
        "data-mapping": "pathname",
        "data-reactions-enabled": "1",
        "data-emit-metadata": "0",
        "data-theme": giscusTheme,
        "data-lang": "en",
        "crossorigin": "anonymous",
        "async": "",
    };

    let giscusScript = document.createElement("script");
    Object.entries(giscusAttributes).forEach(([key, value]) => giscusScript.setAttribute(key, value));
    document.getElementById("comment").appendChild(giscusScript);
</script>

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
<script>
    const getStoredTheme = () => localStorage.getItem("theme") === "dark" ? "dark" : "light";

    const setGiscusTheme = () => {
        const sendMessage = (message) => {
            const iframe = document.querySelector('iframe.giscus-frame');
            if (iframe) {
                iframe.contentWindow.postMessage({ giscus: message }, 'https://giscus.app');
            }
        }
        sendMessage({ setConfig: { theme: getStoredTheme() } })
    }

    document.addEventListener("DOMContentLoaded", () => {
        const giscusAttributes = {
            "src": "https://giscus.app/client.js",
            "data-repo": "[ENTER REPO HERE]",
            "data-repo-id": "[ENTER REPO ID HERE]",
            "data-category": "[ENTER CATEGORY NAME HERE]",
            "data-category-id": "[ENTER CATEGORY ID HERE]",
            "data-mapping": "pathname",
            "data-strict": "0",
            "data-reactions-enabled": "1",
            "data-emit-metadata": "0",
            "data-input-position": "bottom",
            "data-theme": getStoredTheme(),
            "data-lang": "en",
            "data-loading": "lazy",
            "crossorigin": "anonymous",
            "async": "",
        };

        // Dynamically create script tag.
        const giscusScript = document.createElement("script");
        Object.entries(giscusAttributes).forEach(
            ([key, value]) => giscusScript.setAttribute(key, value));
        document.getElementById("comment").appendChild(giscusScript);

        // Update giscus theme when the theme switcher is clicked.
        const themeSwitcher = document.querySelector(".themeswitch");
        if (themeSwitcher) {
            themeSwitcher.addEventListener("click", setGiscusTheme);
        }
    });
</script>

Done and dusted! ๐ŸŽ‰

Ref: 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

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/


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:

References


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ไป“ๅบ“้‡Œใ€‚

ๅ› ไธบๆ—ถ้—ดๅ’Œ่ƒฝๅŠ›ๆœ‰้™๏ผŒ่ฟ™ไบ›้กน็›ฎไฝœไธšๅฟ…ๆœ‰้”™่ฏฏ่€Œไธ”ๅบ”่ฏฅไธไผšๆ”นไบ†๏ผŒๆƒๅฝ“ๅค‡ไปฝไบ†๐ŸŒšใ€‚

็ฉถๅ…ถๅŽŸๅ› ่ฟ˜ๆ˜ฏ่‡ชๅทฑๅคช่œไบ†๏ผŒๆ‰€ไปฅ่ฟ˜่ฆๅฅฝๅฅฝๅญฆๅ•Š๐Ÿ‘€ใ€‚

Credit


File: content/_index.md


title: Homepage

Hey, I'm Jason Lyu

Previously CAS MEng @ McMaster

You can find me on:

FAQ

[TBA]