//go:build linux package tun import ( "net/netip" _ "unsafe" "github.com/sagernet/nftables" "github.com/sagernet/nftables/binaryutil" "github.com/sagernet/nftables/expr" "github.com/sagernet/nftables/userdata" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/ranges" "golang.org/x/exp/slices" "golang.org/x/sys/unix" ) //go:linkname allocSetID github.com/sagernet/nftables.allocSetID var allocSetID uint32 func init() { allocSetID = 6 } func (r *autoRedirect) nftablesCreateAddressSets( nft *nftables.Conn, table *nftables.Table, update bool, ) error { routeAddressSet := *r.routeAddressSet routeExcludeAddressSet := *r.routeExcludeAddressSet if len(routeAddressSet) == 0 && len(routeExcludeAddressSet) == 0 { return nil } if len(routeAddressSet) > 0 { if r.enableIPv4 { _, err := nftablesCreateIPSet(nft, table, 1, "inet4_route_address_set", nftables.TableFamilyIPv4, routeAddressSet, nil, true, update) if err != nil { return err } } if r.enableIPv6 { _, err := nftablesCreateIPSet(nft, table, 2, "inet6_route_address_set", nftables.TableFamilyIPv6, routeAddressSet, nil, true, update) if err != nil { return err } } } if len(routeExcludeAddressSet) > 0 { if r.enableIPv4 { _, err := nftablesCreateIPSet(nft, table, 3, "inet4_route_exclude_address_set", nftables.TableFamilyIPv4, routeExcludeAddressSet, nil, false, update) if err != nil { return err } } if r.enableIPv6 { _, err := nftablesCreateIPSet(nft, table, 4, "inet6_route_exclude_address_set", nftables.TableFamilyIPv6, routeExcludeAddressSet, nil, false, update) if err != nil { return err } } } return nil } func (r *autoRedirect) nftablesCreateLocalAddressSets( nft *nftables.Conn, table *nftables.Table, localAddresses []netip.Prefix, lastAddresses []netip.Prefix, ) error { if r.enableIPv4 { localAddresses4 := common.Filter(localAddresses, func(it netip.Prefix) bool { return it.Addr().Is4() }) updateAddresses4 := common.Filter(localAddresses, func(it netip.Prefix) bool { return it.Addr().Is4() }) var update bool if len(lastAddresses) != 0 { if !slices.Equal(localAddresses4, updateAddresses4) { update = true } } if len(lastAddresses) == 0 || update { _, err := nftablesCreateIPSet(nft, table, 5, "inet4_local_address_set", nftables.TableFamilyIPv4, nil, localAddresses4, false, update) if err != nil { return err } } } if r.enableIPv6 { localAddresses6 := common.Filter(localAddresses, func(it netip.Prefix) bool { return it.Addr().Is6() }) updateAddresses6 := common.Filter(localAddresses, func(it netip.Prefix) bool { return it.Addr().Is6() }) var update bool if len(lastAddresses) != 0 { if !slices.Equal(localAddresses6, updateAddresses6) { update = true } } localAddresses6 = common.Filter(localAddresses6, func(it netip.Prefix) bool { address := it.Addr() return address.IsLoopback() || address.IsGlobalUnicast() && !address.IsPrivate() }) if len(lastAddresses) == 0 || update { _, err := nftablesCreateIPSet(nft, table, 6, "inet6_local_address_set", nftables.TableFamilyIPv6, nil, localAddresses6, false, update) if err != nil { return err } } } return nil } func (r *autoRedirect) nftablesCreateExcludeRules(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain) error { if r.tunOptions.AutoRedirectMarkMode && chain.Hooknum == nftables.ChainHookOutput { nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{ Key: expr.MetaKeyMARK, Register: 1, }, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectOutputMark), }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, }) } if chain.Hooknum == nftables.ChainHookPrerouting { if len(r.tunOptions.IncludeInterface) > 0 { if len(r.tunOptions.IncludeInterface) > 1 { includeInterface := &nftables.Set{ Table: table, Anonymous: true, Constant: true, KeyType: nftables.TypeIFName, } err := nft.AddSet(includeInterface, common.Map(r.tunOptions.IncludeInterface, func(it string) nftables.SetElement { return nftables.SetElement{ Key: nftablesIfname(it), } })) if err != nil { return err } nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, &expr.Lookup{ SourceRegister: 1, SetID: includeInterface.ID, SetName: includeInterface.Name, Invert: true, }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, }) } else { nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, &expr.Cmp{ Op: expr.CmpOpNeq, Register: 1, Data: nftablesIfname(r.tunOptions.IncludeInterface[0]), }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, }) } } if len(r.tunOptions.ExcludeInterface) > 0 { if len(r.tunOptions.ExcludeInterface) > 1 { excludeInterface := &nftables.Set{ Table: table, Anonymous: true, Constant: true, KeyType: nftables.TypeIFName, } err := nft.AddSet(excludeInterface, common.Map(r.tunOptions.ExcludeInterface, func(it string) nftables.SetElement { return nftables.SetElement{ Key: nftablesIfname(it), } })) if err != nil { return err } nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, &expr.Lookup{ SourceRegister: 1, SetID: excludeInterface.ID, SetName: excludeInterface.Name, }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, }) } else { nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, Data: nftablesIfname(r.tunOptions.ExcludeInterface[0]), }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, }) } } } else { if len(r.tunOptions.IncludeUID) > 0 { if len(r.tunOptions.IncludeUID) > 1 || r.tunOptions.IncludeUID[0].Start != r.tunOptions.IncludeUID[0].End { includeUID := &nftables.Set{ Table: table, Anonymous: true, Constant: true, Interval: true, KeyType: nftables.TypeUID, } err := nft.AddSet(includeUID, common.FlatMap(r.tunOptions.IncludeUID, func(it ranges.Range[uint32]) []nftables.SetElement { return []nftables.SetElement{ { Key: binaryutil.NativeEndian.PutUint32(it.Start), }, { Key: binaryutil.NativeEndian.PutUint32(it.End + 1), IntervalEnd: true, }, } })) if err != nil { return err } nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{Key: expr.MetaKeySKUID, Register: 1}, &expr.Lookup{ SourceRegister: 1, SetID: includeUID.ID, SetName: includeUID.Name, Invert: true, }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, UserData: userdata.AppendString(nil, userdata.TypeComment, "not a bug :("), }) } else { nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{Key: expr.MetaKeySKUID, Register: 1}, &expr.Cmp{ Op: expr.CmpOpNeq, Register: 1, Data: binaryutil.BigEndian.PutUint32(r.tunOptions.IncludeUID[0].Start), }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, }) } } if len(r.tunOptions.ExcludeUID) > 0 { if len(r.tunOptions.ExcludeUID) > 1 || r.tunOptions.ExcludeUID[0].Start != r.tunOptions.ExcludeUID[0].End { excludeUID := &nftables.Set{ Table: table, Anonymous: true, Constant: true, Interval: true, KeyType: nftables.TypeUID, } err := nft.AddSet(excludeUID, common.FlatMap(r.tunOptions.ExcludeUID, func(it ranges.Range[uint32]) []nftables.SetElement { return []nftables.SetElement{ { Key: binaryutil.NativeEndian.PutUint32(it.Start), }, { Key: binaryutil.NativeEndian.PutUint32(it.End + 1), IntervalEnd: true, }, } })) if err != nil { return err } nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{Key: expr.MetaKeySKUID, Register: 1}, &expr.Lookup{ SourceRegister: 1, SetID: excludeUID.ID, SetName: excludeUID.Name, }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, UserData: userdata.AppendString(nil, userdata.TypeComment, "not a bug :("), }) } else { nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{Key: expr.MetaKeySKUID, Register: 1}, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.ExcludeUID[0].Start), }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, }) } } } if len(r.tunOptions.Inet4RouteAddress) > 0 { inet4RouteAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv4, nil, r.tunOptions.Inet4RouteAddress, false, false) if err != nil { return err } nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet4RouteAddress.ID, inet4RouteAddress.Name, nftables.TableFamilyIPv4, true) } if len(r.tunOptions.Inet6RouteAddress) > 0 { inet6RouteAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv6, nil, r.tunOptions.Inet6RouteAddress, false, false) if err != nil { return err } nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet6RouteAddress.ID, inet6RouteAddress.Name, nftables.TableFamilyIPv6, true) } if len(r.tunOptions.Inet4RouteExcludeAddress) > 0 { inet4RouteExcludeAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv4, nil, r.tunOptions.Inet4RouteExcludeAddress, false, false) if err != nil { return err } nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet4RouteExcludeAddress.ID, inet4RouteExcludeAddress.Name, nftables.TableFamilyIPv4, false) } if len(r.tunOptions.Inet6RouteExcludeAddress) > 0 { inet6RouteExcludeAddress, err := nftablesCreateIPSet(nft, table, 0, "", nftables.TableFamilyIPv6, nil, r.tunOptions.Inet6RouteExcludeAddress, false, false) if err != nil { return err } nftablesCreateExcludeDestinationIPSet(nft, table, chain, inet6RouteExcludeAddress.ID, inet6RouteExcludeAddress.Name, nftables.TableFamilyIPv6, false) } if !r.tunOptions.EXP_DisableDNSHijack && ((chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeNAT) || (r.tunOptions.AutoRedirectMarkMode && chain.Hooknum == nftables.ChainHookOutput && chain.Type == nftables.ChainTypeNAT)) { if r.enableIPv4 { err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv4) if err != nil { return err } } if r.enableIPv6 { err := r.nftablesCreateDNSHijackRulesForFamily(nft, table, chain, nftables.TableFamilyIPv6) if err != nil { return err } } } if r.tunOptions.AutoRedirectMarkMode && ((chain.Hooknum == nftables.ChainHookOutput && chain.Type == nftables.ChainTypeRoute) || (chain.Hooknum == nftables.ChainHookPrerouting && chain.Type == nftables.ChainTypeFilter)) { nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{ Key: expr.MetaKeyL4PROTO, Register: 1, }, &expr.Cmp{ Op: expr.CmpOpNeq, Register: 1, Data: []byte{unix.IPPROTO_UDP}, }, &expr.Verdict{ Kind: expr.VerdictReturn, }, }, }) } if r.enableIPv4 { nftablesCreateExcludeDestinationIPSet(nft, table, chain, 5, "inet4_local_address_set", nftables.TableFamilyIPv4, false) } if r.enableIPv6 { nftablesCreateExcludeDestinationIPSet(nft, table, chain, 6, "inet6_local_address_set", nftables.TableFamilyIPv6, false) } routeAddressSet := *r.routeAddressSet routeExcludeAddressSet := *r.routeExcludeAddressSet if r.enableIPv4 && len(routeAddressSet) > 0 { nftablesCreateExcludeDestinationIPSet(nft, table, chain, 1, "inet4_route_address_set", nftables.TableFamilyIPv4, true) } if r.enableIPv6 && len(routeAddressSet) > 0 { nftablesCreateExcludeDestinationIPSet(nft, table, chain, 2, "inet6_route_address_set", nftables.TableFamilyIPv6, true) } if r.enableIPv4 && len(routeExcludeAddressSet) > 0 { nftablesCreateExcludeDestinationIPSet(nft, table, chain, 3, "inet4_route_exclude_address_set", nftables.TableFamilyIPv4, false) } if r.enableIPv6 && len(routeExcludeAddressSet) > 0 { nftablesCreateExcludeDestinationIPSet(nft, table, chain, 4, "inet6_route_exclude_address_set", nftables.TableFamilyIPv6, false) } return nil } func (r *autoRedirect) nftablesCreateMark(nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain) { nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Immediate{ Register: 1, Data: binaryutil.NativeEndian.PutUint32(r.tunOptions.AutoRedirectInputMark), }, &expr.Meta{ Key: expr.MetaKeyMARK, Register: 1, SourceRegister: true, }, &expr.Meta{ Key: expr.MetaKeyMARK, Register: 1, }, // output meta mark set myMark ct mark set meta mark &expr.Ct{ Key: expr.CtKeyMARK, Register: 1, SourceRegister: true, }, &expr.Counter{}, }, }) } func (r *autoRedirect) nftablesCreateRedirect( nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, exprs ...expr.Any, ) { if r.enableIPv4 && !r.enableIPv6 { exprs = append(exprs, &expr.Meta{ Key: expr.MetaKeyNFPROTO, Register: 1, }, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, Data: []byte{uint8(nftables.TableFamilyIPv4)}, }) } else if !r.enableIPv4 && r.enableIPv6 { exprs = append(exprs, &expr.Meta{ Key: expr.MetaKeyNFPROTO, Register: 1, }, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, Data: []byte{uint8(nftables.TableFamilyIPv6)}, }) } nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: append(exprs, &expr.Meta{ Key: expr.MetaKeyL4PROTO, Register: 1, }, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_TCP}, }, &expr.Counter{}, &expr.Immediate{ Register: 1, Data: binaryutil.BigEndian.PutUint16(r.redirectPort()), }, &expr.Redir{ RegisterProtoMin: 1, Flags: unix.NF_NAT_RANGE_PROTO_SPECIFIED, }, &expr.Verdict{ Kind: expr.VerdictReturn, }, ), }) } func (r *autoRedirect) nftablesCreateDNSHijackRulesForFamily( nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, family nftables.TableFamily, ) error { ipProto := &nftables.Set{ Table: table, Anonymous: true, Constant: true, KeyType: nftables.TypeInetProto, } err := nft.AddSet(ipProto, []nftables.SetElement{ {Key: []byte{unix.IPPROTO_TCP}}, {Key: []byte{unix.IPPROTO_UDP}}, }) if err != nil { return err } dnsServer := common.Find(r.tunOptions.DNSServers, func(it netip.Addr) bool { return it.Is4() == (family == nftables.TableFamilyIPv4) }) if !dnsServer.IsValid() { if family == nftables.TableFamilyIPv4 { if HasNextAddress(r.tunOptions.Inet4Address[0], 1) { dnsServer = r.tunOptions.Inet4Address[0].Addr().Next() } } else { if HasNextAddress(r.tunOptions.Inet6Address[0], 1) { dnsServer = r.tunOptions.Inet6Address[0].Addr().Next() } } } if !dnsServer.IsValid() { return nil } exprs := []expr.Any{ &expr.Meta{ Key: expr.MetaKeyNFPROTO, Register: 1, }, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, Data: []byte{uint8(family)}, }, } if chain.Hooknum == nftables.ChainHookOutput { // It looks like we can't hijack DNS requests sent to loopback. // https://serverfault.com/questions/363899/iptables-dnat-from-loopback // and tproxy is not available in output exprs = append(exprs, &expr.Meta{ Key: expr.MetaKeyOIFNAME, Register: 1, }, &expr.Cmp{ Op: expr.CmpOpNeq, Register: 1, Data: nftablesIfname("lo"), }, ) } exprs = append(exprs, &expr.Meta{ Key: expr.MetaKeyL4PROTO, Register: 1, }, &expr.Lookup{ SourceRegister: 1, SetID: ipProto.ID, SetName: ipProto.Name, }, &expr.Payload{ OperationType: expr.PayloadLoad, DestRegister: 1, Base: expr.PayloadBaseTransportHeader, Offset: 2, Len: 2, }, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, Data: binaryutil.BigEndian.PutUint16(53), }, &expr.Immediate{ Register: 1, Data: dnsServer.AsSlice(), }, &expr.NAT{ Type: expr.NATTypeDestNAT, Family: uint32(family), RegAddrMin: 1, }, &expr.Counter{}, ) nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: exprs, }) return nil } func (r *autoRedirect) nftablesCreateUnreachable( nft *nftables.Conn, table *nftables.Table, chain *nftables.Chain, ) { if (r.enableIPv4 && r.enableIPv6) || !r.tunOptions.StrictRoute { return } var nfProto nftables.TableFamily if r.enableIPv4 { nfProto = nftables.TableFamilyIPv6 } else { nfProto = nftables.TableFamilyIPv4 } nft.AddRule(&nftables.Rule{ Table: table, Chain: chain, Exprs: []expr.Any{ &expr.Meta{ Key: expr.MetaKeyNFPROTO, Register: 1, }, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, Data: []byte{uint8(nfProto)}, }, &expr.Counter{}, &expr.Verdict{ Kind: expr.VerdictDrop, }, }, }) }