feat: support real process name traffic split (#6)

This commit is contained in:
mzz 2023-02-04 11:24:03 +08:00 committed by GitHub
parent 0793534f72
commit b117dfa7d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 178 additions and 74 deletions

View File

@ -92,6 +92,7 @@ jobs:
- name: Get project dependencies
run: |
git submodule update --init --recursive
go mod download
- name: Build dae

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "component/control/kern/headers"]
path = component/control/kern/headers
url = https://github.com/v2rayA/dae_bpf_headers

View File

@ -4,7 +4,7 @@
***dae***, means goose, is a lightweight and high-performance transparent proxy solution.
In order to improve the traffic diversion performance as much as possible, dae runs the transparent proxy and traffic diversion suite in the linux kernel by eBPF. Therefore, we have the opportunity to make the direct traffic bypass the forwarding by proxy application and achieve true direct traffic through. Under such a magic trick, there is almost no performance loss and additional resource consumption for direct traffic.
In order to improve the traffic split performance as much as possible, dae runs the transparent proxy and traffic split suite in the linux kernel by eBPF. Therefore, we have the opportunity to make the direct traffic bypass the forwarding by proxy application and achieve true direct traffic through. Under such a magic trick, there is almost no performance loss and additional resource consumption for direct traffic.
As a successor of [v2rayA](https://github.com/v2rayA/v2rayA), dae abandoned v2ray-core to meet the needs of users more freely.
@ -43,16 +43,45 @@ Note that if you bind dae to LAN only, dae only provide network service for traf
You need bind dae to WAN interface, if you want dae to provide network service for local programs.
This feature requires kernel version of the machine >= 5.2.
This feature requires kernel version of the machine >= 5.7.
Note that if you bind dae to WAN only, dae only provide network service for local programs and not impact traffic coming in from other interfaces.
### Kernel Configuration Item
Usually, mainstream desktop distributions have these items turned on. But in order to reduce kernel size, some items are turned off by default on embedded device distributions like OpenWRT, Armbian, etc.
Use following commands to check the kernel configuration items on your machine.
```shell
zcat /proc/config.gz || cat /boot/config || cat /boot/config-$(uname -r)
```
**Bind to LAN**
```
CONFIG_DEBUG_INFO_BTF
```
**Bind to WAN**:
```
CONFIG_DEBUG_INFO_BTF
```
Check them using command like:
```shell
(zcat /proc/config.gz || cat /boot/config || cat /boot/config-$(uname -r)) | grep 'CONFIG_DEBUG_INFO_BTF='
```
## TODO
1. Check dns upstream and source loop (whether upstream is also a client of us) and remind the user to add sip rule.
1. Domain routing performance optimization.
1. DisableL4Checksum by link.
1. Handle the case that nodes do not support UDP.
1. Handle the case that nodes do not support IPv6.
1. L4Checksum problem.
1. MACv2 extension extraction.
1. ...

View File

@ -98,4 +98,4 @@ const (
var BasicFeatureVersion = internal.Version{5, 2, 0}
var FtraceFeatureVersion = internal.Version{5, 5, 0}
var CgGetPidFeatureVersion = internal.Version{5, 7, 0}
var CgSocketCookieFeatureVersion = internal.Version{5, 7, 0}

View File

@ -29,11 +29,19 @@ type _bpfPortRange struct {
PortEnd uint16
}
func (r _bpfPortRange) Encode() uint32 {
var b [4]byte
binary.BigEndian.PutUint16(b[:2], r.PortStart)
binary.BigEndian.PutUint16(b[2:], r.PortEnd)
return binary.BigEndian.Uint32(b[:])
type _bpfMatchSet struct {
// TODO: Need sync with C code.
Value [16]byte
Type uint8
Not bool
Outbound uint8
_ [1]byte
}
func (r _bpfPortRange) Encode() (b [16]byte) {
binary.LittleEndian.PutUint16(b[:2], r.PortStart)
binary.LittleEndian.PutUint16(b[2:], r.PortEnd)
return b
}
func (o *bpfObjects) newLpmMap(keys []_bpfLpmKey, values []uint32) (m *ebpf.Map, err error) {

View File

@ -69,6 +69,10 @@ func NewControlPlane(
if kernelVersion.Less(consts.BasicFeatureVersion) {
return nil, fmt.Errorf("your kernel version %v does not satisfy basic requirement; expect >=%v", c.kernelVersion.String(), consts.BasicFeatureVersion.String())
}
if len(wanInterface) > 0 && kernelVersion.Less(consts.CgSocketCookieFeatureVersion) {
return nil, fmt.Errorf("your kernel version %v does not support bind to WAN; expect >=%v; remove wan_interface in config file and try again", kernelVersion.String(),
consts.CgSocketCookieFeatureVersion.String())
}
// Allow the current process to lock memory for eBPF resources.
if err = rlimit.RemoveMemlock(); err != nil {
@ -83,7 +87,8 @@ func NewControlPlane(
var ProgramOptions ebpf.ProgramOptions
if log.IsLevelEnabled(logrus.TraceLevel) {
ProgramOptions = ebpf.ProgramOptions{
LogLevel: ebpf.LogLevelInstruction | ebpf.LogLevelStats,
LogLevel: ebpf.LogLevelBranch | ebpf.LogLevelStats,
//LogLevel: ebpf.LogLevelInstruction | ebpf.LogLevelStats,
}
}
@ -92,7 +97,7 @@ func NewControlPlane(
if len(lanInterface) > 0 && len(wanInterface) == 0 {
// Only bind LAN.
obj = &bpfObjectsLan{}
} else if len(wanInterface) == 0 && len(wanInterface) > 0 {
} else if len(lanInterface) == 0 && len(wanInterface) > 0 {
// Only bind to WAN.
// Trick. Replace the beams with rotten timbers.
obj = &bpfObjectsWan{}

View File

@ -164,10 +164,6 @@ func (c *ControlPlaneCore) BindLan(ifname string) error {
}
func (c *ControlPlaneCore) BindWan(ifname string) error {
if c.kernelVersion.Less(consts.BasicFeatureVersion) {
return fmt.Errorf("your kernel version %v does not support bind to WAN; expect >=%v; remove wan_interface in config file and try again", c.kernelVersion.String(), consts.CgGetPidFeatureVersion.String())
}
c.log.Infof("Bind to WAN: %v", ifname)
link, err := netlink.LinkByName(ifname)
if err != nil {

@ -0,0 +1 @@
Subproject commit 372c3cc61d2d907b89ebdfb7bec180a09cd28169

View File

@ -3,20 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) since 2022, v2rayA Organization <team@v2raya.org>
*/
#include <asm-generic/errno-base.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/in6.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/pkt_cls.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <stdbool.h>
#include "headers/if_ether_defs.h"
#include "headers/pkt_cls_defs.h"
#include "headers/socket_defs.h"
#include "headers/vmlinux.h"
#include <asm-generic/errno-base.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
// #define __DEBUG_ROUTING
// #define __PRINT_ROUTING_RESULT
@ -53,6 +50,8 @@
#define MAX_DST_MAPPING_NUM (65536 * 2)
#define MAX_SRC_PID_PNAME_MAPPING_NUM (65536)
#define IPV6_MAX_EXTENSIONS 4
#define MAX_ARG_LEN_TO_PROBE 192
#define MAX_ARG_SCANNER_BUFFER_SIZE (TASK_COMM_LEN * 4)
#define OUTBOUND_DIRECT 0
#define OUTBOUND_BLOCK 1
@ -229,6 +228,7 @@ struct port_range {
*/
struct match_set {
union {
/// NOTICE: MUST sync with component/control/bpf_utils.go.
__u32 __value; // Placeholder for bpf2go.
__u32 index;
@ -827,13 +827,13 @@ routing(const __u32 flag[6], const void *l4_hdr, const __be32 saddr[4],
/// TODO: BPF_MAP_UPDATE_BATCH ?
__u32 key = MatchType_L4Proto;
if ((ret = bpf_map_update_elem(&l4proto_ipversion_map, &key, &_l4proto_type,
BPF_ANY))) {
if (unlikely((ret = bpf_map_update_elem(&l4proto_ipversion_map, &key,
&_l4proto_type, BPF_ANY)))) {
return ret;
};
key = MatchType_IpVersion;
if ((ret = bpf_map_update_elem(&l4proto_ipversion_map, &key, &_ipversion_type,
BPF_ANY))) {
if (unlikely((ret = bpf_map_update_elem(&l4proto_ipversion_map, &key,
&_ipversion_type, BPF_ANY)))) {
return ret;
};
@ -849,11 +849,13 @@ routing(const __u32 flag[6], const void *l4_hdr, const __be32 saddr[4],
}
key = MatchType_SourcePort;
if ((ret = bpf_map_update_elem(&h_port_map, &key, &h_sport, BPF_ANY))) {
if (unlikely(
(ret = bpf_map_update_elem(&h_port_map, &key, &h_sport, BPF_ANY)))) {
return ret;
};
key = MatchType_Port;
if ((ret = bpf_map_update_elem(&h_port_map, &key, &h_dport, BPF_ANY))) {
if (unlikely(
(ret = bpf_map_update_elem(&h_port_map, &key, &h_dport, BPF_ANY)))) {
return ret;
};
@ -862,7 +864,7 @@ routing(const __u32 flag[6], const void *l4_hdr, const __be32 saddr[4],
if (h_dport == 53 && _l4proto_type == L4ProtoType_UDP) {
struct ip_port *upstream =
bpf_map_lookup_elem(&dns_upstream_map, &zero_key);
if (!upstream) {
if (unlikely(!upstream)) {
return -EFAULT;
}
h_dport = bpf_ntohs(upstream->port);
@ -875,21 +877,21 @@ routing(const __u32 flag[6], const void *l4_hdr, const __be32 saddr[4],
__builtin_memcpy(lpm_key_instance.data, daddr, IPV6_BYTE_LENGTH);
// bpf_printk("mac: %pI6", mac);
key = MatchType_IpSet;
if ((ret = bpf_map_update_elem(&lpm_key_map, &key, &lpm_key_instance,
BPF_ANY))) {
if (unlikely((ret = bpf_map_update_elem(&lpm_key_map, &key, &lpm_key_instance,
BPF_ANY)))) {
return ret;
};
__builtin_memcpy(lpm_key_instance.data, saddr, IPV6_BYTE_LENGTH);
key = MatchType_SourceIpSet;
if ((ret = bpf_map_update_elem(&lpm_key_map, &key, &lpm_key_instance,
BPF_ANY))) {
if (unlikely((ret = bpf_map_update_elem(&lpm_key_map, &key, &lpm_key_instance,
BPF_ANY)))) {
return ret;
};
if (!_is_wan) {
__builtin_memcpy(lpm_key_instance.data, mac, IPV6_BYTE_LENGTH);
key = MatchType_Mac;
if ((ret = bpf_map_update_elem(&lpm_key_map, &key, &lpm_key_instance,
BPF_ANY))) {
if (unlikely((ret = bpf_map_update_elem(&lpm_key_map, &key,
&lpm_key_instance, BPF_ANY)))) {
return ret;
};
}
@ -909,7 +911,7 @@ routing(const __u32 flag[6], const void *l4_hdr, const __be32 saddr[4],
for (__u32 i = 0; i < MAX_MATCH_SET_LEN; i++) {
__u32 k = i; // Clone to pass code checker.
match_set = bpf_map_lookup_elem(&routing_map, &k);
if (!match_set) {
if (unlikely(!match_set)) {
return -EFAULT;
}
if (bad_rule || good_subrule) {
@ -980,8 +982,8 @@ routing(const __u32 flag[6], const void *l4_hdr, const __be32 saddr[4],
if ((domain_routing->bitmap[i / 32] >> (i % 32)) & 1) {
good_subrule = true;
}
} else if (_is_wan && match_set->type == MatchType_ProcessName) {
if ((equal_ipv6_format(match_set->pname, _pname))) {
} else if (match_set->type == MatchType_ProcessName) {
if (_is_wan && equal_ipv6_format(match_set->pname, _pname)) {
good_subrule = true;
}
} else if (match_set->type == MatchType_Final) {
@ -1298,7 +1300,7 @@ static __always_inline bool pid_is_control_plane(struct __sk_buff *skb,
if (p) {
*p = NULL;
}
if ((skb->mark & 0x80) == 0x80) {
if ((skb->mark & 0x100) == 0x100) {
bpf_printk("No pid_pname found. But it should not happen");
/*
if (l4proto == IPPROTO_TCP) {
@ -1934,7 +1936,7 @@ int tproxy_wan_ingress(struct __sk_buff *skb) {
}
static int __always_inline update_map_elem_by_cookie(const __u64 cookie) {
if (!cookie) {
if (unlikely(!cookie)) {
bpf_printk("zero cookie");
return -EINVAL;
}
@ -1943,15 +1945,67 @@ static int __always_inline update_map_elem_by_cookie(const __u64 cookie) {
// Build value.
struct pid_pname val;
__builtin_memset(&val, 0, sizeof(struct pid_pname));
val.pid = bpf_get_current_pid_tgid() >> 32;
// struct task_struct *t = (void *)bpf_get_current_task();
if ((ret = bpf_get_current_comm(val.pname, sizeof(val.pname)))) {
return ret;
char buf[MAX_ARG_SCANNER_BUFFER_SIZE] = {0};
struct task_struct *current = (void *)bpf_get_current_task();
unsigned long arg_start = BPF_CORE_READ(current, mm, arg_start);
unsigned long arg_end = BPF_CORE_READ(current, mm, arg_end);
unsigned long arg_len = arg_end - arg_start;
if (arg_len > MAX_ARG_LEN_TO_PROBE) {
arg_len = MAX_ARG_LEN_TO_PROBE;
}
/**
For string like: /usr/lib/sddm/sddm-helper --socket /tmp/sddm-auth1
We extract "sddm-helper" from it.
*/
unsigned long loc, j;
unsigned long last_slash = -1;
#pragma unroll
for (loc = 0, j = 0; j < MAX_ARG_LEN_TO_PROBE;
++j, loc = ((loc + 1) & (MAX_ARG_SCANNER_BUFFER_SIZE - 1))) {
// volatile unsigned long k = j; // Cheat to unroll.
// if (arg_start + k >= arg_end) {
if (unlikely(arg_start + j >= arg_end)) {
break;
}
if (unlikely(loc == 0)) {
/// WANRING: Do NOT use bpf_core_read_user_str, it will bring terminator
/// 0.
// __builtin_memset(&buf, 0, MAX_ARG_SCANNER_BUFFER_SIZE);
unsigned long to_read = arg_end - (arg_start + j);
if (to_read >= MAX_ARG_SCANNER_BUFFER_SIZE) {
to_read = MAX_ARG_SCANNER_BUFFER_SIZE;
} else {
buf[to_read] = 0;
}
if ((ret = bpf_core_read_user(&buf, to_read, arg_start + j))) {
bpf_printk("failed to read process name: %d", ret);
return ret;
}
}
if (unlikely(buf[loc] == '/')) {
last_slash = j;
} else if (unlikely(buf[loc] == ' ' || buf[loc] == 0)) {
break;
}
}
++last_slash;
unsigned long length_cpy = j - last_slash;
if (length_cpy > TASK_COMM_LEN) {
length_cpy = TASK_COMM_LEN;
}
if ((ret = bpf_core_read_user(&val.pname, length_cpy,
arg_start + last_slash))) {
bpf_printk("failed to read process name: %d", ret);
return ret;
}
val.pid = BPF_CORE_READ(current, tgid);
// bpf_printk("a start_end: %lu %lu", arg_start, arg_end);
// bpf_printk("b start_end: %lu %lu", arg_start + last_slash, arg_start + j);
// Update map.
if ((ret =
bpf_map_update_elem(&cookie_pid_map, &cookie, &val, BPF_NOEXIST))) {
if (unlikely(ret = bpf_map_update_elem(&cookie_pid_map, &cookie, &val,
BPF_NOEXIST))) {
// bpf_printk("setup_mapping_from_sk: failed update map: %d", ret);
return ret;
}
@ -1970,7 +2024,7 @@ int tproxy_wan_cg_sock_create(struct bpf_sock *sk) {
SEC("cgroup/sock_release")
int tproxy_wan_cg_sock_release(struct bpf_sock *sk) {
__u64 cookie = bpf_get_socket_cookie(sk);
if (!cookie) {
if (unlikely(!cookie)) {
bpf_printk("zero cookie");
return 1;
}

View File

@ -6,6 +6,7 @@
package control
import (
"encoding/binary"
"fmt"
"github.com/cilium/ebpf"
"github.com/v2rayA/dae/common"
@ -14,7 +15,6 @@ import (
"github.com/v2rayA/dae/pkg/config_parser"
"net/netip"
"strconv"
"unsafe"
)
type DomainSet struct {
@ -27,7 +27,7 @@ type RoutingMatcherBuilder struct {
*routing.DefaultMatcherBuilder
outboundName2Id map[string]uint8
bpf *bpfObjects
rules []bpfMatchSet
rules []_bpfMatchSet
SimulatedLpmTries [][]netip.Prefix
SimulatedDomainSet []DomainSet
Final string
@ -74,7 +74,7 @@ func (b *RoutingMatcherBuilder) AddDomain(f *config_parser.Function, key string,
RuleIndex: len(b.rules),
Domains: values,
})
b.rules = append(b.rules, bpfMatchSet{
b.rules = append(b.rules, _bpfMatchSet{
Type: uint8(consts.MatchType_DomainSet),
Not: f.Not,
Outbound: b.OutboundToId(outbound),
@ -94,12 +94,14 @@ func (b *RoutingMatcherBuilder) AddSourceMac(f *config_parser.Function, macAddrs
}
lpmTrieIndex := len(b.SimulatedLpmTries)
b.SimulatedLpmTries = append(b.SimulatedLpmTries, values)
b.rules = append(b.rules, bpfMatchSet{
set := _bpfMatchSet{
Value: [16]byte{},
Type: uint8(consts.MatchType_Mac),
Value: uint32(lpmTrieIndex),
Not: f.Not,
Outbound: b.OutboundToId(outbound),
})
}
binary.LittleEndian.PutUint32(set.Value[:], uint32(lpmTrieIndex))
b.rules = append(b.rules, set)
}
@ -109,12 +111,14 @@ func (b *RoutingMatcherBuilder) AddIp(f *config_parser.Function, values []netip.
}
lpmTrieIndex := len(b.SimulatedLpmTries)
b.SimulatedLpmTries = append(b.SimulatedLpmTries, values)
b.rules = append(b.rules, bpfMatchSet{
set := _bpfMatchSet{
Value: [16]byte{},
Type: uint8(consts.MatchType_IpSet),
Value: uint32(lpmTrieIndex),
Not: f.Not,
Outbound: b.OutboundToId(outbound),
})
}
binary.LittleEndian.PutUint32(set.Value[:], uint32(lpmTrieIndex))
b.rules = append(b.rules, set)
}
func (b *RoutingMatcherBuilder) AddPort(f *config_parser.Function, values [][2]uint16, _outbound string) {
@ -123,7 +127,7 @@ func (b *RoutingMatcherBuilder) AddPort(f *config_parser.Function, values [][2]u
if i == len(values)-1 {
outbound = _outbound
}
b.rules = append(b.rules, bpfMatchSet{
b.rules = append(b.rules, _bpfMatchSet{
Type: uint8(consts.MatchType_Port),
Value: _bpfPortRange{
PortStart: value[0],
@ -141,12 +145,14 @@ func (b *RoutingMatcherBuilder) AddSourceIp(f *config_parser.Function, values []
}
lpmTrieIndex := len(b.SimulatedLpmTries)
b.SimulatedLpmTries = append(b.SimulatedLpmTries, values)
b.rules = append(b.rules, bpfMatchSet{
set := _bpfMatchSet{
Value: [16]byte{},
Type: uint8(consts.MatchType_SourceIpSet),
Value: uint32(lpmTrieIndex),
Not: f.Not,
Outbound: b.OutboundToId(outbound),
})
}
binary.LittleEndian.PutUint32(set.Value[:], uint32(lpmTrieIndex))
b.rules = append(b.rules, set)
}
func (b *RoutingMatcherBuilder) AddSourcePort(f *config_parser.Function, values [][2]uint16, _outbound string) {
@ -155,7 +161,7 @@ func (b *RoutingMatcherBuilder) AddSourcePort(f *config_parser.Function, values
if i == len(values)-1 {
outbound = _outbound
}
b.rules = append(b.rules, bpfMatchSet{
b.rules = append(b.rules, _bpfMatchSet{
Type: uint8(consts.MatchType_SourcePort),
Value: _bpfPortRange{
PortStart: value[0],
@ -171,9 +177,9 @@ func (b *RoutingMatcherBuilder) AddL4Proto(f *config_parser.Function, values con
if b.err != nil {
return
}
b.rules = append(b.rules, bpfMatchSet{
b.rules = append(b.rules, _bpfMatchSet{
Value: [16]byte{byte(values)},
Type: uint8(consts.MatchType_L4Proto),
Value: uint32(values),
Not: f.Not,
Outbound: b.OutboundToId(outbound),
})
@ -183,9 +189,9 @@ func (b *RoutingMatcherBuilder) AddIpVersion(f *config_parser.Function, values c
if b.err != nil {
return
}
b.rules = append(b.rules, bpfMatchSet{
b.rules = append(b.rules, _bpfMatchSet{
Value: [16]byte{byte(values)},
Type: uint8(consts.MatchType_IpVersion),
Value: uint32(values),
Not: f.Not,
Outbound: b.OutboundToId(outbound),
})
@ -197,12 +203,12 @@ func (b *RoutingMatcherBuilder) AddProcessName(f *config_parser.Function, values
if i == len(values)-1 {
outbound = _outbound
}
matchSet := bpfMatchSet{
matchSet := _bpfMatchSet{
Type: uint8(consts.MatchType_ProcessName),
Not: f.Not,
Outbound: b.OutboundToId(outbound),
}
copy((*(*[16]byte)(unsafe.Pointer(&matchSet.Value)))[:], value[:])
copy(matchSet.Value[:], value[:])
b.rules = append(b.rules, matchSet)
}
}
@ -212,7 +218,7 @@ func (b *RoutingMatcherBuilder) AddFinal(outbound string) {
return
}
b.Final = outbound
b.rules = append(b.rules, bpfMatchSet{
b.rules = append(b.rules, _bpfMatchSet{
Type: uint8(consts.MatchType_Final),
Outbound: b.OutboundToId(outbound),
})

View File

@ -106,7 +106,7 @@ func init() {
func SoMarkControl(c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
//TODO: force to set 0xff. any chances to customize this value?
err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, fwmarkIoctl, 0x80)
err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, fwmarkIoctl, 0x100)
if err != nil {
return
}

View File

@ -65,7 +65,8 @@ routing {
# See routing.md for full examples.
ip(1.1.1.1) && port(53) -> my_group
pname(firefox) && domain(ip.sb) -> direct # pname like firefox not works yet [ FIXME ]
pname(firefox) && domain(ip.sb) -> direct
pname(curl) && domain(ip.sb) -> my_group
ip(geoip:private) -> direct

View File

@ -45,7 +45,7 @@ ipversion(6) -> ipv6_group
# Source MAC rule
mac('02:42:ac:11:00:02') -> direct
# Process Name rule (Only support local process)
# Process Name rule (only support local process)
pname(curl) -> direct
# Multiple domains rule