Skip to content

Conversation

Amaindex
Copy link
Contributor

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 Amaindex force-pushed the main branch 2 times, most recently from 1675d2b to 62ceb0c Compare April 19, 2023 07:38
@xjasonlyu xjasonlyu changed the title Feat: Add support for multicast (#243) Feature: Add support for multicast Apr 19, 2023
@Amaindex
Copy link
Contributor Author

Any maintainer reviewed this code? :S

@xjasonlyu
Copy link
Owner

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

@Amaindex
Copy link
Contributor Author

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
Copy link
Owner

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

@xjasonlyu xjasonlyu removed the Stale label Jul 30, 2023
@xjasonlyu
Copy link
Owner

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

@xjasonlyu xjasonlyu merged commit 90f7754 into xjasonlyu:main Aug 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants