ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
7 changed files with 145336 additions and 0 deletions
175
packages/mesh-init/abi.go
Normal file
175
packages/mesh-init/abi.go
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
xtHeader = 32
|
||||
ruleHead = 112
|
||||
)
|
||||
|
||||
type tableInfo struct {
|
||||
Name [32]byte
|
||||
Valid uint32
|
||||
Hooks [5]uint32
|
||||
Underflow [5]uint32
|
||||
Count uint32
|
||||
Size uint32
|
||||
}
|
||||
|
||||
type replaceHead struct {
|
||||
Name [32]byte
|
||||
Valid uint32
|
||||
Count uint32
|
||||
Size uint32
|
||||
Hooks [5]uint32
|
||||
Underflow [5]uint32
|
||||
OldCount uint32
|
||||
Address uint64
|
||||
}
|
||||
|
||||
type ipMatch struct {
|
||||
Addresses [4]uint32
|
||||
In [16]byte
|
||||
Out [16]byte
|
||||
InMask [16]byte
|
||||
OutMask [16]byte
|
||||
Protocol uint16
|
||||
Flags uint8
|
||||
Inverse uint8
|
||||
}
|
||||
|
||||
type counter struct {
|
||||
Packets uint64
|
||||
Bytes uint64
|
||||
}
|
||||
|
||||
type ruleHeader struct {
|
||||
IP ipMatch
|
||||
Cache uint32
|
||||
Target uint16
|
||||
Next uint16
|
||||
From uint32
|
||||
Counters counter
|
||||
}
|
||||
|
||||
type replacement struct {
|
||||
data []byte
|
||||
counters []counter
|
||||
pins runtime.Pinner
|
||||
}
|
||||
|
||||
func encode(value any) []byte {
|
||||
data, err := binary.Append(nil, binary.NativeEndian, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func size16(size int) uint16 {
|
||||
if size < 0 || size > math.MaxUint16 {
|
||||
panic("iptables size exceeds uint16")
|
||||
}
|
||||
return uint16(size)
|
||||
}
|
||||
|
||||
func size32(size int) uint32 {
|
||||
if size < 0 || uint64(size) > math.MaxUint32 {
|
||||
panic("iptables size exceeds uint32")
|
||||
}
|
||||
return uint32(size)
|
||||
}
|
||||
|
||||
func extension(name string, revision uint8, data []byte) []byte {
|
||||
size := (xtHeader + len(data) + 7) &^ 7
|
||||
header := struct {
|
||||
Size uint16
|
||||
Name [29]byte
|
||||
Revision uint8
|
||||
}{Size: size16(size), Revision: revision}
|
||||
copy(header.Name[:], name)
|
||||
result := append(encode(header), data...)
|
||||
return append(result, make([]byte, size-len(result))...)
|
||||
}
|
||||
|
||||
func accept() []byte {
|
||||
return extension("", 0, encode(int32(-2)))
|
||||
}
|
||||
|
||||
func redirect(port uint16) []byte {
|
||||
rangeInfo := struct {
|
||||
Count uint32
|
||||
Flags uint32
|
||||
IPs [2]uint32
|
||||
Ports [4]byte
|
||||
}{Count: 1, Flags: 2}
|
||||
binary.BigEndian.PutUint16(rangeInfo.Ports[:2], port)
|
||||
binary.BigEndian.PutUint16(rangeInfo.Ports[2:], port)
|
||||
return extension("REDIRECT", 0, encode(rangeInfo))
|
||||
}
|
||||
|
||||
func tcp(port uint16) []byte {
|
||||
return extension("tcp", 0, encode(struct {
|
||||
Source [2]uint16
|
||||
Dest [2]uint16
|
||||
Flags [4]uint8
|
||||
}{Source: [2]uint16{0, math.MaxUint16}, Dest: [2]uint16{port, port}}))
|
||||
}
|
||||
|
||||
func owner(uid uint32) []byte {
|
||||
return extension("owner", 1, encode(struct {
|
||||
UID [2]uint32
|
||||
GID [2]uint32
|
||||
Match uint8
|
||||
Invert uint8
|
||||
Pad [2]byte
|
||||
}{UID: [2]uint32{uid, uid}, Match: 1}))
|
||||
}
|
||||
|
||||
func rule(protocol uint16, match, target []byte, loopback bool) []byte {
|
||||
header := ruleHeader{
|
||||
IP: ipMatch{Protocol: protocol},
|
||||
Target: size16(ruleHead + len(match)),
|
||||
Next: size16(ruleHead + len(match) + len(target)),
|
||||
}
|
||||
if loopback {
|
||||
copy(header.IP.Out[:], "lo")
|
||||
header.IP.OutMask[0], header.IP.OutMask[1], header.IP.OutMask[2] = 255, 255, 255
|
||||
}
|
||||
return append(append(encode(header), match...), target...)
|
||||
}
|
||||
|
||||
func table(value config, count uint32, address uint64) []byte {
|
||||
header := replaceHead{Valid: 27, OldCount: count, Address: address}
|
||||
copy(header.Name[:], "nat")
|
||||
var body []byte
|
||||
add := func(data []byte) {
|
||||
body = append(body, data...)
|
||||
header.Count++
|
||||
}
|
||||
for _, hook := range []int{0, 1, 3, 4} {
|
||||
header.Hooks[hook] = size32(len(body))
|
||||
switch hook {
|
||||
case 0:
|
||||
for _, port := range value.exclude {
|
||||
add(rule(6, tcp(port), accept(), false))
|
||||
}
|
||||
add(rule(6, nil, redirect(value.inbound), false))
|
||||
case 3:
|
||||
add(rule(0, owner(value.proxyUID), accept(), false))
|
||||
add(rule(0, nil, accept(), true))
|
||||
add(rule(6, nil, redirect(value.outbound), false))
|
||||
}
|
||||
header.Underflow[hook] = size32(len(body))
|
||||
add(rule(0, nil, accept(), false))
|
||||
}
|
||||
var label [30]byte
|
||||
copy(label[:], "ERROR")
|
||||
add(rule(0, nil, extension("ERROR", 0, label[:]), false))
|
||||
header.Size = size32(len(body))
|
||||
return append(encode(header), body...)
|
||||
}
|
||||
97
packages/mesh-init/abi_test.go
Normal file
97
packages/mesh-init/abi_test.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLayout(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
value any
|
||||
size int
|
||||
}{
|
||||
{tableInfo{}, 84}, {replaceHead{}, 96}, {ipMatch{}, 84}, {ruleHeader{}, 112}, {counter{}, 16},
|
||||
} {
|
||||
if size := binary.Size(test.value); size != test.size {
|
||||
t.Errorf("%T size = %d, want %d", test.value, size, test.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTable(t *testing.T) {
|
||||
value := config{1234, 5678, 9012, []uint16{80, 443}}
|
||||
data := table(value, 5, 0x12345678)
|
||||
var head replaceHead
|
||||
if err := binary.Read(bytes.NewReader(data), binary.NativeEndian, &head); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(bytes.TrimRight(head.Name[:], "\x00")) != "nat" || head.Valid != 27 || head.Count != 11 || int(head.Size) != len(data)-96 || head.OldCount != 5 || head.Address != 0x12345678 {
|
||||
t.Fatalf("invalid replacement header: %+v", head)
|
||||
}
|
||||
body := data[96:]
|
||||
starts := map[uint32]bool{}
|
||||
var entries [][]byte
|
||||
for offset := 0; offset < len(body); {
|
||||
var rule ruleHeader
|
||||
if err := binary.Read(bytes.NewReader(body[offset:]), binary.NativeEndian, &rule); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rule.Next%8 != 0 || rule.Target < 112 || rule.Next < rule.Target+32 {
|
||||
t.Fatalf("invalid rule offsets: %+v", rule)
|
||||
}
|
||||
starts[size32(offset)] = true
|
||||
entries = append(entries, body[offset:offset+int(rule.Next)])
|
||||
offset += int(rule.Next)
|
||||
}
|
||||
if len(entries) != 11 {
|
||||
t.Fatalf("entry count = %d", len(entries))
|
||||
}
|
||||
for _, hook := range []int{0, 1, 3, 4} {
|
||||
if !starts[head.Hooks[hook]] || !starts[head.Underflow[hook]] {
|
||||
t.Errorf("hook %d does not point to entry boundaries", hook)
|
||||
}
|
||||
policy := body[head.Underflow[hook]:]
|
||||
if verdict := binary.NativeEndian.Uint32(policy[144:]); verdict != math.MaxUint32-1 {
|
||||
t.Errorf("hook %d has non-ACCEPT policy", hook)
|
||||
}
|
||||
}
|
||||
for index, port := range map[int]uint16{0: 80, 1: 443} {
|
||||
if got := binary.NativeEndian.Uint16(entries[index][148:]); got != port {
|
||||
t.Errorf("inbound exclusion = %d, want %d", got, port)
|
||||
}
|
||||
}
|
||||
if binary.BigEndian.Uint16(entries[2][160:]) != value.inbound || binary.BigEndian.Uint16(entries[7][160:]) != value.outbound {
|
||||
t.Error("redirect ports differ from configuration")
|
||||
}
|
||||
if binary.NativeEndian.Uint32(entries[5][144:]) != value.proxyUID || !bytes.Equal(entries[6][32:35], []byte{'l', 'o', 0}) || !bytes.Equal(entries[6][64:67], []byte{255, 255, 255}) {
|
||||
t.Error("proxy UID or loopback exemption differs")
|
||||
}
|
||||
if !bytes.Contains(entries[10], []byte("ERROR")) {
|
||||
t.Error("missing table terminator")
|
||||
}
|
||||
without := table(config{inbound: 1, outbound: 2}, 3, 0)
|
||||
if got := binary.NativeEndian.Uint32(without[36:]); got != 9 {
|
||||
t.Errorf("empty exclusions produce %d entries", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSizeBounds(t *testing.T) {
|
||||
if size16(math.MaxUint16) != math.MaxUint16 || size32(math.MaxUint32) != math.MaxUint32 {
|
||||
t.Fatal("maximum ABI sizes differ")
|
||||
}
|
||||
for _, check := range []func(){
|
||||
func() { extension("tcp", 0, make([]byte, 65535)) },
|
||||
func() { size16(-1) }, func() { size32(-1) }, func() { size32(math.MaxUint32 + 1) },
|
||||
} {
|
||||
func() {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Error("unrepresentable ABI size accepted")
|
||||
}
|
||||
}()
|
||||
check()
|
||||
}()
|
||||
}
|
||||
}
|
||||
3
packages/mesh-init/go.mod
Normal file
3
packages/mesh-init/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module quorate.dev/packages/mesh-init
|
||||
|
||||
go 1.27.2
|
||||
56
packages/mesh-init/main.go
Normal file
56
packages/mesh-init/main.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
inbound uint16
|
||||
outbound uint16
|
||||
proxyUID uint32
|
||||
exclude []uint16
|
||||
}
|
||||
|
||||
func parse(args []string) (config, error) {
|
||||
flags := flag.NewFlagSet("mesh-init", flag.ContinueOnError)
|
||||
flags.SetOutput(io.Discard)
|
||||
inbound := flags.Uint("inbound", 4143, "inbound proxy port")
|
||||
outbound := flags.Uint("outbound", 4140, "outbound proxy port")
|
||||
uid := flags.Uint64("proxy-uid", 2102, "proxy user ID")
|
||||
exclude := flags.String("ignore-inbound", "4190,4191", "inbound ports excluded from interception")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
if flags.NArg() != 0 || *inbound == 0 || *inbound > math.MaxUint16 || *outbound == 0 || *outbound > math.MaxUint16 || *uid > math.MaxUint32 {
|
||||
return config{}, fmt.Errorf("invalid proxy configuration")
|
||||
}
|
||||
result := config{inbound: uint16(*inbound), outbound: uint16(*outbound), proxyUID: uint32(*uid)}
|
||||
if *exclude != "" {
|
||||
for _, item := range strings.Split(*exclude, ",") {
|
||||
port, err := strconv.ParseUint(item, 10, 16)
|
||||
if err != nil || port == 0 {
|
||||
return config{}, fmt.Errorf("invalid inbound exclusion %q", item)
|
||||
}
|
||||
result.exclude = append(result.exclude, uint16(port))
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
value, err := parse(os.Args[1:])
|
||||
if err == nil {
|
||||
err = install(value)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("NAT_READY")
|
||||
}
|
||||
36
packages/mesh-init/main_test.go
Normal file
36
packages/mesh-init/main_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
args []string
|
||||
want config
|
||||
}{
|
||||
{"defaults", nil, config{4143, 4140, 2102, []uint16{4190, 4191}}},
|
||||
{"custom", []string{"--inbound=10123", "--outbound=10456", "--proxy-uid=9876", "--ignore-inbound=80,443"}, config{10123, 10456, 9876, []uint16{80, 443}}},
|
||||
{"bounds", []string{"--inbound=65535", "--outbound=1", "--proxy-uid=4294967295", "--ignore-inbound="}, config{math.MaxUint16, 1, math.MaxUint32, nil}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := parse(test.args)
|
||||
if err != nil || !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("parse = %+v, %v; want %+v", got, err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, args := range [][]string{
|
||||
{"--inbound=0"}, {"--outbound=0"}, {"--inbound=65536"}, {"--outbound=-1"},
|
||||
{"--proxy-uid=4294967296"}, {"--proxy-uid=-1"},
|
||||
{"--ignore-inbound=0"}, {"--ignore-inbound=80,"}, {"--ignore-inbound=65536"},
|
||||
{"--ignore-inbound=garbage"}, {"--unknown"}, {"extra"},
|
||||
} {
|
||||
if _, err := parse(args); err == nil {
|
||||
t.Errorf("accepted %v", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/mesh-init/nat_linux.go
Normal file
71
packages/mesh-init/nat_linux.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
getInfo = 64
|
||||
setReplace = 64
|
||||
)
|
||||
|
||||
func readTable(fd int) (tableInfo, error) {
|
||||
var info tableInfo
|
||||
copy(info.Name[:], "nat")
|
||||
size := uint32(unsafe.Sizeof(info))
|
||||
// #nosec G103 -- Direct Syscall6 pointer arguments remain fixed until the call returns.
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, uintptr(fd), syscall.IPPROTO_IP, getInfo, uintptr(unsafe.Pointer(&info)), uintptr(unsafe.Pointer(&size)), 0)
|
||||
if err != 0 {
|
||||
return tableInfo{}, err
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func newReplace(value config, count uint32) *replacement {
|
||||
result := &replacement{counters: make([]counter, count)}
|
||||
// ipt_replace embeds this address, outside the syscall's directly pinned arguments.
|
||||
result.pins.Pin(&result.counters[0])
|
||||
result.data = table(value, count, result.address())
|
||||
return result
|
||||
}
|
||||
|
||||
func (value *replacement) address() uint64 {
|
||||
// #nosec G103 -- newReplace pins this buffer until the replacement syscall completes.
|
||||
return uint64(uintptr(unsafe.Pointer(&value.counters[0])))
|
||||
}
|
||||
|
||||
func writeTable(fd int, value *replacement) error {
|
||||
// #nosec G103 -- The payload is a direct syscall argument; its embedded counter address is pinned.
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_SETSOCKOPT, uintptr(fd), syscall.IPPROTO_IP, setReplace, uintptr(unsafe.Pointer(&value.data[0])), uintptr(len(value.data)), 0)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func install(value config) (err error) {
|
||||
if unsafe.Sizeof(uintptr(0)) != 8 {
|
||||
return fmt.Errorf("unsupported iptables ABI")
|
||||
}
|
||||
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW|syscall.SOCK_CLOEXEC, syscall.IPPROTO_RAW)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open NAT socket: %w", err)
|
||||
}
|
||||
defer func() { err = errors.Join(err, syscall.Close(fd)) }()
|
||||
info, err := readTable(fd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read NAT table: %w", err)
|
||||
}
|
||||
if info.Count == 0 {
|
||||
return fmt.Errorf("NAT table has no counter entries")
|
||||
}
|
||||
update := newReplace(value, info.Count)
|
||||
defer update.pins.Unpin()
|
||||
if err := writeTable(fd, update); err != nil {
|
||||
return fmt.Errorf("install NAT table: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
39
packages/mesh-init/nat_linux_test.go
Normal file
39
packages/mesh-init/nat_linux_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"runtime"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Keep each recursive frame live so the stack must grow past its initial allocation.
|
||||
//
|
||||
//go:noinline
|
||||
func growStack(depth byte) byte {
|
||||
var frame [4096]byte
|
||||
frame[0] = depth
|
||||
if depth > 0 {
|
||||
frame[1] = growStack(depth - 1)
|
||||
}
|
||||
runtime.KeepAlive(&frame)
|
||||
return frame[0] + frame[1]
|
||||
}
|
||||
|
||||
func TestPinnedRequest(t *testing.T) {
|
||||
if unsafe.Sizeof(tableInfo{}) != 84 {
|
||||
t.Fatal("native getsockopt layout differs from ipt_getinfo")
|
||||
}
|
||||
done := make(chan [2]uint64, 1)
|
||||
go func() {
|
||||
request := newReplace(config{inbound: 4143, outbound: 4140}, 5)
|
||||
defer request.pins.Unpin()
|
||||
growStack(32)
|
||||
runtime.GC()
|
||||
done <- [2]uint64{binary.NativeEndian.Uint64(request.data[88:]), request.address()}
|
||||
}()
|
||||
addresses := <-done
|
||||
if addresses[0] != addresses[1] {
|
||||
t.Fatalf("counter address moved: %x -> %x", addresses[0], addresses[1])
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue