xjasonlyu / tun2socks

tun2socks - powered by gVisor TCP/IP stack
https://github.com/xjasonlyu/tun2socks/wiki
GNU General Public License v3.0
2.85k stars 404 forks source link

Feature: Add support for multicast #245

Closed Amaindex closed 10 months ago

Amaindex commented 1 year ago

We have implemented a lightweight tunnel that supports multicast message transmission based on a patch for tun2socks. I organized the patch as a PR and hope it will be helpful for the development of tun2socks :)

Implementation

To enable tun2socks to handle multicast messages, we need to make the following efforts:

First, we add multicast-groups as a startup parameter and implement the corresponding parser . Its format is a comma-separated list of IP addresses, e.g., 225.0.0.1,ff02::1.

// tun2socks/main
flag.StringVar(&key.MulticastGroups, "multicast-groups", "", "Join these multicast groups ip1,ip2,...")

// tun2socks/engine/parse
func parseMulticastGroups(multicastGroupsString string) ([]net.IP, error) {
    if multicastGroupsString == "" {
        return []net.IP{}, nil
    }
    ipStrings := strings.Split(multicastGroupsString, ",")
    multicastGroups := []net.IP{}
    for _, ipString := range ipStrings {
        ip := net.ParseIP(ipString)
        if ip == nil {
            return nil, fmt.Errorf("unsupported ip format: %s", ipString)
        }
        if !ip.IsMulticast() {
            return nil, fmt.Errorf("invalid multicast address: %s", ipString)
        }
        multicastGroups = append(multicastGroups, ip)
    }
    return multicastGroups, nil
}

Then pass the parsed net.IP slice multicastGroups as configuration to core.CreateStack.


// tun2socks/engine/engine
var multicastGroups []net.IP = nil
if multicastGroups, err = parseMulticastGroups(k.MulticastGroups); err != nil {
    return err
}
if _defaultStack, err = core.CreateStack(&core.Config{
    LinkEndpoint:     _defaultDevice,
    TransportHandler: &mirror.Tunnel{},
    MulticastGroups:  multicastGroups,
    Options:          opts,
}); err != nil {
    return
}

withMulticastGroups is implemented to add the default nic to the given multicast groups.

func withMulticastGroups(nicID tcpip.NICID, multicastGroups []net.IP) option.Option {
    return func(s *stack.Stack) error {
        if multicastGroups == nil{
            return nil
        }
        s.AddProtocolAddress(
            nicID,
            tcpip.ProtocolAddress{
                Protocol: ipv4.ProtocolNumber,
                AddressWithPrefix: tcpip.AddressWithPrefix{
                    Address:   "\x0A\x00\x00\x01",
                    PrefixLen: 8,
                },
            },
            stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint},
        )
        s.AddProtocolAddress(
            nicID,
            tcpip.ProtocolAddress{
                Protocol: ipv6.ProtocolNumber,
                AddressWithPrefix: tcpip.AddressWithPrefix{
                    Address:   "\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01",
                    PrefixLen: 8,
                },
            },
            stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint},
        )
        for _, multicastGroup := range multicastGroups {
            if tcpIpAddr := multicastGroup.To4(); tcpIpAddr != nil {
                if err := s.JoinGroup(ipv4.ProtocolNumber, nicID, tcpip.Address(tcpIpAddr)); err != nil {
                    return fmt.Errorf("join multicast groups: %s", err)
                }
            } else {
                tcpIpAddr := multicastGroup.To16()
                if err := s.JoinGroup(ipv6.ProtocolNumber, nicID, tcpip.Address(tcpIpAddr)); err != nil {
                    return fmt.Errorf("join multicast groups: %s", err)
                }
            }
        }
        return nil
    }
}

NOTE: Why add addresses to the default nic?

By default, tun2socks uses the UDP Forwarder provided by gVisor to generate ForwarderRequest. When calling CreateEndpoint, both ep.net.Bind and ep.net.Connect will be applied to be able to pass the response back to the data sender. When forwarding multicast data, e.g. 198.18.0.1:60355 <-> 225.0.0.1:1234, the UDP Endpoint will bind to 225.0.0.1 and try to connect to 198.18.0.1.

The default nic of tun2 is working on Spoofing mode. When the UDP Endpoint tries to use a non-local address to connect, the network stack will generate a temporary addressState to build the route, which can be primary but is ephemeral. Nevertheless, when the UDP Endpoint tries to use a multicast address to connect, the network stack will select an available primary addressState to build the route. However, when tun2socks is in the just-initialized or idle state, there will be no available primary addressState, and the connect operation will fail. Therefore, we need to add permanent addresses, e.g. 10.0.0.1/8 and fd00:1/8, to the default nic , which are only used to build routes for multicast response and do not affect other connections.

In fact, for multicast, the sender normally does not expect a response. So, the ep.net.Connect is unnecessary. If we implement a custom UDP Forwarder and ForwarderRequest in the future, we can remove these code.

Testing

I have conducted preliminary testing on this patch using Python scripts and a standard SOCKS5 server in a Linux environment, with the following setup:

Create TUN interface tun0 and assign an IP address for it.

ip tuntap add mode tun dev tun0
ip addr add 198.18.0.1/15 dev tun0
ip link set dev tun0 up

Start tun2socks.

tun2socks -device tun0 -proxy socks5://host:port -multicast-groups 225.0.0.1,ff02::1 -loglevel debug

Test ipv4.

import socket

multicast_addr = ('225.0.0.1',12345)

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton('198.18.0.1'))

message = b"Hello, multicast!"
sock.sendto(message, multicast_addr)

sock.close()

Logs.

# tun2socks
INFO[0035] [UDP] 198.18.0.1:56927 <-> 225.0.0.1:12345

# tcpdump
IP 198.18.0.1.56927 > 225.0.0.1.12345: UDP, length 17

# socks5server
Send datagram to (225.0.0.1,12345)

Test ipv6.

import socket

multicast_addr = ('ff02::1', 12345)

sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex('tun0'))

message = b'Hello, multicast!'
sock.sendto(message, multicast_addr)

sock.close()

Logs.

# tun2socks
INFO[0269] [UDP] [fe80::263a:bb0d:39a3:c5f2]:35957 <-> [ff02::1]:12345

# tcpdump
IP6 fe80::263a:bb0d:39a3:c5f2.35957 > ip6-allnodes.12345: UDP, length 17

# socks5server
Send datagram to (ff02::1,12345)
Amaindex commented 1 year ago

Any maintainer reviewed this code? :S

xjasonlyu commented 1 year ago

Hi @Amaindex, thanks for your PR! I’ll review and test this feature later :-)

Amaindex commented 11 months ago

hi @xjasonlyu. It looks like this PR has been put on hold. Are there any critical blockers preventing its progress, and what can we do to move this feature forward?

xjasonlyu commented 11 months ago

Sorry, this PR is not on hold. I just don't have enough time to test and review it yet.

xjasonlyu commented 10 months ago

I made some adjustments to fit the latest gvisor API. LGTM, thanks!