ce7cb702218693d08ea6d83464d2457c9250027c

This commit is contained in:
ms2k 2026-11-12 20:46:28 +09:00
commit 34c5f8963c
7 changed files with 145336 additions and 0 deletions

175
packages/mesh-init/abi.go Normal file
View 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...)
}

View 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()
}()
}
}

View file

@ -0,0 +1,3 @@
module quorate.dev/packages/mesh-init
go 1.27.2

View 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")
}

View 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)
}
}
}

View 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
}

View 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])
}
}