quorate/services/short/tunnel/network.go

159 lines
6 KiB
Go

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
}