package tunnel import ( "encoding/binary" "fmt" "net" "github.com/google/nftables" "github.com/google/nftables/expr" "github.com/vishvananda/netlink" "github.com/vishvananda/netlink/nl" "golang.org/x/sys/unix" "quorate.dev/services/short/config" ) func forwarding(link netlink.Link) error { req := nl.NewNetlinkRequest(unix.RTM_NEWLINK, unix.NLM_F_ACK) info := nl.NewIfInfomsg(unix.AF_UNSPEC) info.Index = int32(link.Attrs().Index) req.AddData(info) af := nl.NewRtAttr(unix.IFLA_AF_SPEC|unix.NLA_F_NESTED, nil) inet := nl.NewRtAttrChild(af, unix.AF_INET|unix.NLA_F_NESTED, nil) conf := nl.NewRtAttrChild(inet, unix.IFLA_INET_CONF|unix.NLA_F_NESTED, nil) // Linux UAPI IPV4_DEVCONF_FORWARDING is the first IPv4 interface setting. nl.NewRtAttrChild(conf, 1, nl.Uint32Attr(1)) req.AddData(af) _, err := req.Execute(unix.NETLINK_ROUTE, 0) return err } func network(p config.Path) error { if err := p.Validate(); err != nil { return err } links := make([]netlink.Link, 2) for index := range links { name := fmt.Sprintf("ipsec%d", index) link, err := netlink.LinkByName(name) if _, ok := err.(netlink.LinkNotFoundError); ok { link = &netlink.Xfrmi{LinkAttrs: netlink.LinkAttrs{Name: name, MTU: 1280}, Ifid: uint32(42 + index)} if err = netlink.LinkAdd(link); err != nil { return err } } else if err != nil { return err } if err = netlink.LinkSetUp(link); err != nil { return err } links[index] = link } lo, err := netlink.LinkByName("lo") if err != nil { return err } addr, err := netlink.ParseAddr(p.Address + "/32") if err != nil { return err } if err = netlink.AddrReplace(lo, addr); err != nil { return err } eth, err := netlink.LinkByName("eth0") if err != nil { return err } for _, device := range append(links, eth) { if err = forwarding(device); err != nil { return fmt.Errorf("enable interface forwarding: %w", err) } } return firewall(p) } func returnRoute(network string, index int) error { link, err := netlink.LinkByName(fmt.Sprintf("ipsec%d", index)) if err != nil { return err } _, destination, err := net.ParseCIDR(network) if err != nil { return err } return netlink.RouteReplace(&netlink.Route{LinkIndex: link.Attrs().Index, Dst: destination}) } func inputInterface(name string) []expr.Any { value := make([]byte, 16) copy(value, name) return []expr.Any{&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: value}} } func clampMSS(name string, key expr.MetaKey) []expr.Any { value := make([]byte, 16) copy(value, name) return []expr.Any{ &expr.Meta{Key: key, Register: 1}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: value}, &expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_TCP}}, &expr.Payload{DestRegister: 1, Base: expr.PayloadBaseTransportHeader, Offset: 13, Len: 1}, &expr.Bitwise{DestRegister: 1, SourceRegister: 1, Len: 1, Mask: []byte{0x02}, Xor: []byte{0}}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{0x02}}, &expr.Rt{Register: 1, Key: expr.RtTCPMSS}, &expr.Byteorder{DestRegister: 1, SourceRegister: 1, Op: expr.ByteorderHton, Len: 2, Size: 2}, &expr.Exthdr{SourceRegister: 1, Type: 2, Offset: 2, Len: 2, Op: expr.ExthdrOpTcpopt}, } } func dest(address string, port uint16) []expr.Any { buf := make([]byte, 2) binary.BigEndian.PutUint16(buf, port) return []expr.Any{ &expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte{unix.IPPROTO_TCP}}, &expr.Payload{DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, Offset: 16, Len: 4}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: net.ParseIP(address).To4()}, &expr.Payload{DestRegister: 1, Base: expr.PayloadBaseTransportHeader, Offset: 2, Len: 2}, &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: buf}, } } func firewall(p config.Path) error { c := &nftables.Conn{} tables, err := c.ListTables() if err != nil { return err } for _, t := range tables { if t.Name == "short" && t.Family == nftables.TableFamilyIPv4 { c.DelTable(t) } } t := c.AddTable(&nftables.Table{Name: "short", Family: nftables.TableFamilyIPv4}) chain := func(name string, kind nftables.ChainType, hook *nftables.ChainHook, priority *nftables.ChainPriority) *nftables.Chain { return c.AddChain(&nftables.Chain{Name: name, Table: t, Type: kind, Hooknum: hook, Priority: priority}) } pre := chain("dnat", nftables.ChainTypeNAT, nftables.ChainHookPrerouting, nftables.ChainPriorityNATDest) post := chain("snat", nftables.ChainTypeNAT, nftables.ChainHookPostrouting, nftables.ChainPriorityNATSource) forward := chain("forward", nftables.ChainTypeFilter, nftables.ChainHookForward, nftables.ChainPriorityFilter) input := chain("input", nftables.ChainTypeFilter, nftables.ChainHookInput, nftables.ChainPriorityFilter) for index := range 2 { device := fmt.Sprintf("ipsec%d", index) for _, key := range []expr.MetaKey{expr.MetaKeyIIFNAME, expr.MetaKeyOIFNAME} { c.AddRule(&nftables.Rule{Table: t, Chain: forward, Exprs: clampMSS(device, key)}) } for _, port := range p.Ports { match := append(inputInterface(device), dest(p.Address, port)...) match = append(match, &expr.Immediate{Register: 1, Data: net.ParseIP(p.Gateway).To4()}, &expr.NAT{Type: expr.NATTypeDestNAT, Family: unix.NFPROTO_IPV4, RegAddrMin: 1}) c.AddRule(&nftables.Rule{Table: t, Chain: pre, Exprs: match}) match = append(inputInterface(device), dest(p.Gateway, port)...) c.AddRule(&nftables.Rule{Table: t, Chain: post, Exprs: append(match, &expr.Masq{})}) match = append(inputInterface(device), dest(p.Gateway, port)...) c.AddRule(&nftables.Rule{Table: t, Chain: forward, Exprs: append(match, &expr.Verdict{Kind: expr.VerdictAccept})}) } for _, chain := range []*nftables.Chain{forward, input} { c.AddRule(&nftables.Rule{Table: t, Chain: chain, Exprs: append(inputInterface(device), &expr.Verdict{Kind: expr.VerdictDrop})}) } } if err := c.Flush(); err != nil { return fmt.Errorf("apply pod forwarding: %w", err) } return nil }