userspace_linux.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. //go:build linux
  2. package ike
  3. import (
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net"
  10. "os"
  11. "os/exec"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "time"
  16. "golang.org/x/sys/unix"
  17. )
  18. const userspaceTunnelMTU = 1380
  19. type linuxUserspaceInstaller struct {
  20. ipCommand string
  21. }
  22. type linuxUserspaceHandle struct {
  23. ipCommand string
  24. config ChildSAConfig
  25. tunnel *espTunnel
  26. tun *os.File
  27. relay NATTPacketRelay
  28. runContext context.Context
  29. cancel context.CancelFunc
  30. wait sync.WaitGroup
  31. cancelOnce sync.Once
  32. closeOnce sync.Once
  33. mu sync.Mutex
  34. closed bool
  35. terminalErr error
  36. failures chan error
  37. cleanup []ipCleanupCommand
  38. }
  39. type ipCleanupCommand struct {
  40. operation string
  41. arguments []string
  42. }
  43. func (*linuxUserspaceHandle) DataplaneMode() string { return "userspace" }
  44. func (installer linuxUserspaceInstaller) Install(
  45. ctx context.Context,
  46. config ChildSAConfig,
  47. ) (ChildSAHandle, error) {
  48. if ctx == nil {
  49. ctx = context.Background()
  50. }
  51. if config.Relay == nil {
  52. return nil, errors.New("ike: user-space ESP requires a NAT-T packet relay")
  53. }
  54. if !config.UDPEncapsulation {
  55. return nil, errors.New("ike: user-space ESP relay requires negotiated UDP encapsulation")
  56. }
  57. if len(config.PCSCF) == 0 {
  58. return nil, errors.New("ike: user-space ESP requires at least one negotiated P-CSCF address")
  59. }
  60. if err := validateUserspaceRoutes(config); err != nil {
  61. return nil, err
  62. }
  63. command := strings.TrimSpace(installer.ipCommand)
  64. if command == "" {
  65. command = "ip"
  66. }
  67. if _, err := exec.LookPath(command); err != nil {
  68. return nil, errors.New("Linux iproute2 is required to configure the user-space CHILD_SA")
  69. }
  70. tunnel, err := newESPTunnel(config, nil)
  71. if err != nil {
  72. return nil, err
  73. }
  74. tun, actualName, err := openLinuxTUN(config.Name)
  75. if err != nil {
  76. return nil, err
  77. }
  78. config.Name = actualName
  79. runContext, cancel := context.WithCancel(context.Background())
  80. handle := &linuxUserspaceHandle{
  81. ipCommand: command,
  82. config: cloneChildSAConfig(config),
  83. tunnel: tunnel,
  84. tun: tun,
  85. relay: config.Relay,
  86. runContext: runContext,
  87. cancel: cancel,
  88. failures: make(chan error, 1),
  89. }
  90. if err := handle.configure(ctx); err != nil {
  91. cancel()
  92. handle.cleanupNetwork(context.Background())
  93. _ = tun.Close()
  94. return nil, err
  95. }
  96. handle.wait.Add(2)
  97. go handle.copyTUNToRelay()
  98. go handle.copyRelayToTUN()
  99. return handle, nil
  100. }
  101. func openLinuxTUN(name string) (*os.File, string, error) {
  102. name = strings.TrimSpace(name)
  103. if name == "" {
  104. return nil, "", errors.New("ike: TUN interface name is required")
  105. }
  106. request, err := unix.NewIfreq(name)
  107. if err != nil {
  108. return nil, "", fmt.Errorf("ike: invalid TUN interface name: %w", err)
  109. }
  110. request.SetUint16(uint16(unix.IFF_TUN | unix.IFF_NO_PI))
  111. descriptor, err := unix.Open("/dev/net/tun", unix.O_RDWR|unix.O_CLOEXEC, 0)
  112. if err != nil {
  113. return nil, "", fmt.Errorf("ike: open /dev/net/tun: %w", err)
  114. }
  115. if err := unix.IoctlIfreq(descriptor, unix.TUNSETIFF, request); err != nil {
  116. _ = unix.Close(descriptor)
  117. return nil, "", fmt.Errorf("ike: create TUN interface: %w", err)
  118. }
  119. file := os.NewFile(uintptr(descriptor), "/dev/net/tun:"+request.Name())
  120. if file == nil {
  121. _ = unix.Close(descriptor)
  122. return nil, "", errors.New("ike: create TUN file handle")
  123. }
  124. return file, request.Name(), nil
  125. }
  126. func validateUserspaceRoutes(config ChildSAConfig) error {
  127. if config.InnerLocalIPv4 == nil && config.InnerLocalIPv6 == nil {
  128. return errors.New("ike: user-space ESP requires an assigned inner address")
  129. }
  130. validLocal := func(ip net.IP) bool {
  131. return ip != nil &&
  132. !ip.IsUnspecified() &&
  133. !ip.IsMulticast() &&
  134. ipAllowedBySelectors(ip, config.InitiatorSelectors)
  135. }
  136. if config.InnerLocalIPv4 != nil && !validLocal(config.InnerLocalIPv4) {
  137. return errors.New("ike: assigned inner IPv4 address is outside initiator traffic selectors")
  138. }
  139. if config.InnerLocalIPv6 != nil && !validLocal(config.InnerLocalIPv6) {
  140. return errors.New("ike: assigned inner IPv6 address is outside initiator traffic selectors")
  141. }
  142. matchingFamily := false
  143. for _, pcscf := range config.PCSCF {
  144. if pcscf == nil || pcscf.IsUnspecified() || pcscf.IsMulticast() {
  145. return errors.New("ike: P-CSCF address is invalid")
  146. }
  147. if !ipAllowedBySelectors(pcscf, config.ResponderSelectors) {
  148. return fmt.Errorf("ike: P-CSCF %s is outside responder traffic selectors", pcscf)
  149. }
  150. if (pcscf.To4() != nil && config.InnerLocalIPv4 != nil) ||
  151. (pcscf.To4() == nil && pcscf.To16() != nil && config.InnerLocalIPv6 != nil) {
  152. matchingFamily = true
  153. }
  154. }
  155. if !matchingFamily {
  156. return errors.New("ike: no P-CSCF address matches an assigned inner address family")
  157. }
  158. return nil
  159. }
  160. func ipAllowedBySelectors(ip net.IP, selectors []trafficSelector) bool {
  161. for _, selector := range selectors {
  162. if ipWithinRange(ip, selector.StartIP, selector.EndIP) {
  163. return true
  164. }
  165. }
  166. return false
  167. }
  168. func (handle *linuxUserspaceHandle) configure(ctx context.Context) error {
  169. name := handle.config.Name
  170. if handle.config.InnerLocalIPv4 != nil {
  171. if err := handle.run(
  172. ctx,
  173. "assign TUN IPv4 address",
  174. "-4", "address", "add",
  175. handle.config.InnerLocalIPv4.String()+"/32",
  176. "dev", name,
  177. "noprefixroute",
  178. ); err != nil {
  179. return err
  180. }
  181. }
  182. if handle.config.InnerLocalIPv6 != nil {
  183. prefix := handle.config.InnerIPv6Prefix
  184. if prefix == 0 || prefix > 128 {
  185. prefix = 128
  186. }
  187. if err := handle.run(
  188. ctx,
  189. "assign TUN IPv6 address",
  190. "-6", "address", "add",
  191. fmt.Sprintf("%s/%d", handle.config.InnerLocalIPv6.String(), prefix),
  192. "dev", name,
  193. "noprefixroute",
  194. ); err != nil {
  195. return err
  196. }
  197. }
  198. if err := handle.run(
  199. ctx,
  200. "enable TUN interface",
  201. "link", "set", "dev", name,
  202. "mtu", strconv.Itoa(userspaceTunnelMTU),
  203. "up",
  204. ); err != nil {
  205. return err
  206. }
  207. table, priority := userspaceRoutingIdentifiers(handle.config.InboundSPI)
  208. if handle.config.InnerLocalIPv4 != nil {
  209. if err := handle.configureFamily(
  210. ctx,
  211. "-4",
  212. handle.config.InnerLocalIPv4,
  213. handle.ipv4PCSCF(),
  214. 32,
  215. table,
  216. priority,
  217. ); err != nil {
  218. return err
  219. }
  220. }
  221. if handle.config.InnerLocalIPv6 != nil {
  222. if err := handle.configureFamily(
  223. ctx,
  224. "-6",
  225. handle.config.InnerLocalIPv6,
  226. handle.ipv6PCSCF(),
  227. 128,
  228. table,
  229. priority,
  230. ); err != nil {
  231. return err
  232. }
  233. }
  234. return nil
  235. }
  236. func (handle *linuxUserspaceHandle) configureFamily(
  237. ctx context.Context,
  238. family string,
  239. local net.IP,
  240. pcscf []net.IP,
  241. bits int,
  242. table uint32,
  243. priority uint32,
  244. ) error {
  245. if len(pcscf) == 0 {
  246. return nil
  247. }
  248. tableValue := strconv.FormatUint(uint64(table), 10)
  249. priorityValue := strconv.FormatUint(uint64(priority), 10)
  250. localPrefix := fmt.Sprintf("%s/%d", local.String(), bits)
  251. if err := handle.requireUnusedRoutingSlot(
  252. ctx,
  253. family,
  254. tableValue,
  255. priorityValue,
  256. ); err != nil {
  257. return err
  258. }
  259. ruleArguments := []string{
  260. family, "rule", "add",
  261. "priority", priorityValue,
  262. "from", localPrefix,
  263. "lookup", tableValue,
  264. }
  265. if err := handle.run(ctx, "install fail-closed source rule", ruleArguments...); err != nil {
  266. return err
  267. }
  268. handle.recordCleanup(
  269. "remove fail-closed source rule",
  270. family, "rule", "delete",
  271. "priority", priorityValue,
  272. "from", localPrefix,
  273. "lookup", tableValue,
  274. )
  275. unreachableArguments := []string{
  276. family, "route", "add",
  277. "table", tableValue,
  278. "unreachable", "default",
  279. }
  280. if err := handle.run(ctx, "install fail-closed route", unreachableArguments...); err != nil {
  281. return err
  282. }
  283. handle.recordCleanup(
  284. "remove fail-closed route",
  285. family, "route", "delete",
  286. "table", tableValue,
  287. "unreachable", "default",
  288. )
  289. for _, address := range pcscf {
  290. hostPrefix := fmt.Sprintf("%s/%d", address.String(), bits)
  291. routeArguments := []string{
  292. family, "route", "add",
  293. "table", tableValue,
  294. hostPrefix,
  295. "dev", handle.config.Name,
  296. "src", local.String(),
  297. }
  298. if err := handle.run(ctx, "install P-CSCF host route", routeArguments...); err != nil {
  299. return err
  300. }
  301. handle.recordCleanup(
  302. "remove P-CSCF host route",
  303. family, "route", "delete",
  304. "table", tableValue,
  305. hostPrefix,
  306. "dev", handle.config.Name,
  307. "src", local.String(),
  308. )
  309. }
  310. return nil
  311. }
  312. func userspaceRoutingIdentifiers(spi uint32) (table uint32, priority uint32) {
  313. table = spi
  314. if table <= 255 {
  315. table |= 0x80000000
  316. }
  317. // Linux evaluates policy rules from the lowest numeric priority upward.
  318. // The built-in main/default rules are 32766/32767, so a full-width SPI
  319. // used directly as the priority would usually run too late and leak the
  320. // inner source through the host's default route. Keep a SPI-derived slot
  321. // strictly ahead of main; requireUnusedRoutingSlot rejects collisions.
  322. priority = 10000 + spi%20000
  323. return table, priority
  324. }
  325. func (handle *linuxUserspaceHandle) requireUnusedRoutingSlot(
  326. ctx context.Context,
  327. family string,
  328. table string,
  329. priority string,
  330. ) error {
  331. routeCommand := exec.CommandContext(
  332. ctx,
  333. handle.ipCommand,
  334. family, "-j", "route", "show", "table", "all",
  335. )
  336. routeOutput, routeErr := routeCommand.CombinedOutput()
  337. if routeErr != nil {
  338. message := strings.TrimSpace(string(routeOutput))
  339. if message == "" {
  340. message = routeErr.Error()
  341. }
  342. return fmt.Errorf("ike: inspect routing table %s: %s", table, message)
  343. }
  344. var routes []map[string]any
  345. if err := json.Unmarshal(routeOutput, &routes); err != nil {
  346. return fmt.Errorf("ike: parse Linux routing table inventory: %w", err)
  347. }
  348. for _, route := range routes {
  349. value, exists := route["table"]
  350. if !exists {
  351. continue
  352. }
  353. if routingTableValue(value) == table {
  354. return fmt.Errorf("ike: routing table %s is already in use", table)
  355. }
  356. }
  357. ruleCommand := exec.CommandContext(ctx, handle.ipCommand, family, "rule", "show")
  358. ruleOutput, err := ruleCommand.CombinedOutput()
  359. if err != nil {
  360. message := strings.TrimSpace(string(ruleOutput))
  361. if message == "" {
  362. message = err.Error()
  363. }
  364. return fmt.Errorf("ike: inspect policy rules: %s", message)
  365. }
  366. prefix := priority + ":"
  367. for _, line := range strings.Split(string(ruleOutput), "\n") {
  368. fields := strings.Fields(line)
  369. if strings.HasPrefix(strings.TrimSpace(line), prefix) ||
  370. containsAdjacentFields(fields, "lookup", table) {
  371. return fmt.Errorf("ike: policy rule priority %s is already in use", priority)
  372. }
  373. }
  374. return nil
  375. }
  376. func routingTableValue(value any) string {
  377. switch typed := value.(type) {
  378. case float64:
  379. if typed >= 0 && typed <= float64(^uint32(0)) {
  380. return strconv.FormatUint(uint64(typed), 10)
  381. }
  382. case string:
  383. return typed
  384. }
  385. return ""
  386. }
  387. func containsAdjacentFields(fields []string, first string, second string) bool {
  388. for index := 0; index+1 < len(fields); index++ {
  389. if fields[index] == first && fields[index+1] == second {
  390. return true
  391. }
  392. }
  393. return false
  394. }
  395. func (handle *linuxUserspaceHandle) ipv4PCSCF() []net.IP {
  396. var result []net.IP
  397. seen := make(map[string]struct{})
  398. for _, address := range handle.config.PCSCF {
  399. if address.To4() != nil {
  400. if _, duplicate := seen[address.String()]; duplicate {
  401. continue
  402. }
  403. result = append(result, append(net.IP(nil), address...))
  404. seen[address.String()] = struct{}{}
  405. }
  406. }
  407. return result
  408. }
  409. func (handle *linuxUserspaceHandle) ipv6PCSCF() []net.IP {
  410. var result []net.IP
  411. seen := make(map[string]struct{})
  412. for _, address := range handle.config.PCSCF {
  413. if address.To4() == nil && address.To16() != nil {
  414. if _, duplicate := seen[address.String()]; duplicate {
  415. continue
  416. }
  417. result = append(result, append(net.IP(nil), address...))
  418. seen[address.String()] = struct{}{}
  419. }
  420. }
  421. return result
  422. }
  423. func (handle *linuxUserspaceHandle) run(
  424. ctx context.Context,
  425. operation string,
  426. arguments ...string,
  427. ) error {
  428. command := exec.CommandContext(ctx, handle.ipCommand, arguments...)
  429. output, err := command.CombinedOutput()
  430. if err != nil {
  431. message := strings.TrimSpace(string(output))
  432. if message == "" {
  433. message = err.Error()
  434. }
  435. return fmt.Errorf("ike: %s: %s", operation, message)
  436. }
  437. return nil
  438. }
  439. func (handle *linuxUserspaceHandle) recordCleanup(operation string, arguments ...string) {
  440. handle.cleanup = append(handle.cleanup, ipCleanupCommand{
  441. operation: operation,
  442. arguments: append([]string(nil), arguments...),
  443. })
  444. }
  445. func (handle *linuxUserspaceHandle) copyTUNToRelay() {
  446. defer handle.wait.Done()
  447. buffer := make([]byte, 65535)
  448. for {
  449. count, err := handle.tun.Read(buffer)
  450. if err != nil {
  451. if handle.runContext.Err() == nil && !errors.Is(err, os.ErrClosed) {
  452. handle.fail(fmt.Errorf("ike: read TUN packet: %w", err))
  453. }
  454. return
  455. }
  456. protected, err := handle.tunnel.seal(buffer[:count])
  457. if err != nil {
  458. // The kernel may emit IPv6 DAD/link-local traffic when the TUN is
  459. // brought up, and local processes may attempt unrelated routes.
  460. // Traffic-selector enforcement is a filter, not a session failure.
  461. if errors.Is(err, errESPPolicyDrop) {
  462. continue
  463. }
  464. handle.fail(err)
  465. return
  466. }
  467. if err := handle.relay.SendESP(handle.runContext, protected); err != nil {
  468. if handle.runContext.Err() == nil {
  469. handle.fail(fmt.Errorf("ike: relay outbound ESP: %w", err))
  470. }
  471. return
  472. }
  473. }
  474. }
  475. func (handle *linuxUserspaceHandle) copyRelayToTUN() {
  476. defer handle.wait.Done()
  477. buffer := make([]byte, 65535)
  478. for {
  479. count, err := handle.relay.ReceiveESP(handle.runContext, buffer)
  480. if err != nil {
  481. if handle.runContext.Err() == nil {
  482. handle.fail(fmt.Errorf("ike: relay inbound ESP: %w", err))
  483. }
  484. return
  485. }
  486. cleartext, err := handle.tunnel.open(buffer[:count])
  487. if err != nil {
  488. // Invalid ICVs, replays, malformed padding, and packets outside the
  489. // negotiated selectors are untrusted network input. Drop them
  490. // without allowing a forged datagram to tear down the CHILD_SA.
  491. continue
  492. }
  493. if err := writeFull(handle.tun, cleartext); err != nil {
  494. if handle.runContext.Err() == nil && !errors.Is(err, os.ErrClosed) {
  495. handle.fail(fmt.Errorf("ike: write TUN packet: %w", err))
  496. }
  497. return
  498. }
  499. }
  500. }
  501. func writeFull(destination io.Writer, packet []byte) error {
  502. count, err := destination.Write(packet)
  503. if err != nil {
  504. return err
  505. }
  506. if count != len(packet) {
  507. return io.ErrShortWrite
  508. }
  509. return nil
  510. }
  511. func (handle *linuxUserspaceHandle) fail(err error) {
  512. handle.mu.Lock()
  513. notify := false
  514. if handle.terminalErr == nil {
  515. handle.terminalErr = err
  516. notify = true
  517. }
  518. handle.mu.Unlock()
  519. if notify {
  520. select {
  521. case handle.failures <- err:
  522. default:
  523. }
  524. }
  525. handle.cancelRun()
  526. }
  527. func (handle *linuxUserspaceHandle) Failures() <-chan error {
  528. return handle.failures
  529. }
  530. func (handle *linuxUserspaceHandle) cancelRun() {
  531. handle.cancelOnce.Do(func() {
  532. handle.cancel()
  533. })
  534. }
  535. func (handle *linuxUserspaceHandle) closeTUN() {
  536. handle.closeOnce.Do(func() {
  537. _ = handle.tun.Close()
  538. })
  539. }
  540. func (handle *linuxUserspaceHandle) Close(ctx context.Context) error {
  541. handle.mu.Lock()
  542. if handle.closed {
  543. handle.mu.Unlock()
  544. return nil
  545. }
  546. handle.closed = true
  547. handle.mu.Unlock()
  548. handle.cancelRun()
  549. cleanupErr := handle.cleanupNetwork(ctx)
  550. handle.closeTUN()
  551. handle.wait.Wait()
  552. // A terminal data-plane error is delivered exactly once through Failures.
  553. // Close reports only teardown errors so the orchestrator does not record
  554. // the same runtime cause again as a cleanup failure.
  555. return cleanupErr
  556. }
  557. func (handle *linuxUserspaceHandle) cleanupNetwork(ctx context.Context) error {
  558. if ctx == nil || ctx.Err() != nil {
  559. ctx = context.Background()
  560. }
  561. ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
  562. defer cancel()
  563. var errs []error
  564. for index := len(handle.cleanup) - 1; index >= 0; index-- {
  565. item := handle.cleanup[index]
  566. command := exec.CommandContext(ctx, handle.ipCommand, item.arguments...)
  567. if output, err := command.CombinedOutput(); err != nil {
  568. message := strings.TrimSpace(string(output))
  569. if message == "" {
  570. message = err.Error()
  571. }
  572. errs = append(errs, fmt.Errorf("ike: %s: %s", item.operation, message))
  573. }
  574. }
  575. handle.cleanup = nil
  576. return errors.Join(errs...)
  577. }
  578. var _ ChildSAInstaller = linuxUserspaceInstaller{}
  579. var _ ChildSAHandle = (*linuxUserspaceHandle)(nil)
  580. var _ DataplaneEvidence = (*linuxUserspaceHandle)(nil)
  581. var _ DataplaneFailureNotifier = (*linuxUserspaceHandle)(nil)