about summary refs log tree commit diff
diff options
context:
space:
mode:
authorLuke Granger-Brown <git@lukegb.com>2021-05-03 09:48:07 +0100
committerGitHub <noreply@github.com>2021-05-03 09:48:07 +0100
commitd922cad4d6161081e803c82f18ff2967db4123cb (patch)
tree4583a04da8bcbc285ddbb1def08e5777f0bad1c3
parentd688023a47197823b67953d6035b236787ec2a33 (diff)
parentda69e884b8d19835958a04b53cf8148ba8866e66 (diff)
Merge pull request #119172 from midchildan/package/trafficserver
nixos/trafficserver: init
-rw-r--r--maintainers/maintainer-list.nix6
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/web-servers/trafficserver.nix318
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/trafficserver.nix176
-rw-r--r--pkgs/development/libraries/cjose/default.nix40
-rw-r--r--pkgs/servers/http/trafficserver/default.nix207
-rw-r--r--pkgs/top-level/all-packages.nix4
8 files changed, 753 insertions, 0 deletions
diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix
index af40e5cc68bd3..ab81f9eaabfab 100644
--- a/maintainers/maintainer-list.nix
+++ b/maintainers/maintainer-list.nix
@@ -4747,6 +4747,12 @@
     githubId = 1102396;
     name = "Jussi Maki";
   };
+  joaquinito2051 = {
+    email = "joaquinito2051@gmail.com";
+    github = "heroku-miraheze";
+    githubId = 61781343;
+    name = "JoaquĆ­n Rufo Gutierrez";
+  };
   jobojeha = {
     email = "jobojeha@jeppener.de";
     github = "jobojeha";
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 3a82899232011..88228c799870b 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -977,6 +977,7 @@
   ./services/web-servers/shellinabox.nix
   ./services/web-servers/tomcat.nix
   ./services/web-servers/traefik.nix
+  ./services/web-servers/trafficserver.nix
   ./services/web-servers/ttyd.nix
   ./services/web-servers/uwsgi.nix
   ./services/web-servers/varnish/default.nix
diff --git a/nixos/modules/services/web-servers/trafficserver.nix b/nixos/modules/services/web-servers/trafficserver.nix
new file mode 100644
index 0000000000000..db0e2ac0bd05a
--- /dev/null
+++ b/nixos/modules/services/web-servers/trafficserver.nix
@@ -0,0 +1,318 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.trafficserver;
+  user = config.users.users.trafficserver.name;
+  group = config.users.groups.trafficserver.name;
+
+  getManualUrl = name: "https://docs.trafficserver.apache.org/en/latest/admin-guide/files/${name}.en.html";
+  getConfPath = name: "${pkgs.trafficserver}/etc/trafficserver/${name}";
+
+  yaml = pkgs.formats.yaml { };
+
+  fromYAML = f:
+    let
+      jsonFile = pkgs.runCommand "in.json"
+        {
+          nativeBuildInputs = [ pkgs.remarshal ];
+        } ''
+        yaml2json < "${f}" > "$out"
+      '';
+    in
+    builtins.fromJSON (builtins.readFile jsonFile);
+
+  mkYamlConf = name: cfg:
+    if cfg != null then {
+      "trafficserver/${name}.yaml".source = yaml.generate "${name}.yaml" cfg;
+    } else {
+      "trafficserver/${name}.yaml".text = "";
+    };
+
+  mkRecordLines = path: value:
+    if isAttrs value then
+      lib.mapAttrsToList (n: v: mkRecordLines (path ++ [ n ]) v) value
+    else if isInt value then
+      "CONFIG ${concatStringsSep "." path} INT ${toString value}"
+    else if isFloat value then
+      "CONFIG ${concatStringsSep "." path} FLOAT ${toString value}"
+    else
+      "CONFIG ${concatStringsSep "." path} STRING ${toString value}";
+
+  mkRecordsConfig = cfg: concatStringsSep "\n" (flatten (mkRecordLines [ ] cfg));
+  mkPluginConfig = cfg: concatStringsSep "\n" (map (p: "${p.path} ${p.arg}") cfg);
+in
+{
+  options.services.trafficserver = {
+    enable = mkEnableOption "Apache Traffic Server";
+
+    cache = mkOption {
+      type = types.lines;
+      default = "";
+      example = "dest_domain=example.com suffix=js action=never-cache";
+      description = ''
+        Caching rules that overrule the origin's caching policy.
+
+        Consult the <link xlink:href="${getManualUrl "cache.config"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    hosting = mkOption {
+      type = types.lines;
+      default = "";
+      example = "domain=example.com volume=1";
+      description = ''
+        Partition the cache according to origin server or domain
+
+        Consult the <link xlink:href="${getManualUrl "hosting.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    ipAllow = mkOption {
+      type = types.nullOr yaml.type;
+      default = fromYAML (getConfPath "ip_allow.yaml");
+      defaultText = "upstream defaults";
+      example = literalExample {
+        ip_allow = [{
+          apply = "in";
+          ip_addrs = "127.0.0.1";
+          action = "allow";
+          methods = "ALL";
+        }];
+      };
+      description = ''
+        Control client access to Traffic Server and Traffic Server connections
+        to upstream servers.
+
+        Consult the <link xlink:href="${getManualUrl "ip_allow.yaml"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    logging = mkOption {
+      type = types.nullOr yaml.type;
+      default = fromYAML (getConfPath "logging.yaml");
+      defaultText = "upstream defaults";
+      example = literalExample { };
+      description = ''
+        Configure logs.
+
+        Consult the <link xlink:href="${getManualUrl "logging.yaml"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    parent = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        dest_domain=. method=get parent="p1.example:8080; p2.example:8080" round_robin=true
+      '';
+      description = ''
+        Identify the parent proxies used in an cache hierarchy.
+
+        Consult the <link xlink:href="${getManualUrl "parent.config"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    plugins = mkOption {
+      default = [ ];
+
+      description = ''
+        Controls run-time loadable plugins available to Traffic Server, as
+        well as their configuration.
+
+        Consult the <link xlink:href="${getManualUrl "plugin.config"}">upstream
+        documentation</link> for more details.
+      '';
+
+      type = with types;
+        listOf (submodule {
+          options.path = mkOption {
+            type = str;
+            example = "xdebug.so";
+            description = ''
+              Path to plugin. The path can either be absolute, or relative to
+              the plugin directory.
+            '';
+          };
+          options.arg = mkOption {
+            type = str;
+            default = "";
+            example = "--header=ATS-My-Debug";
+            description = "arguments to pass to the plugin";
+          };
+        });
+    };
+
+    records = mkOption {
+      type = with types;
+        let valueType = (attrsOf (oneOf [ int float str valueType ])) // {
+          description = "Traffic Server records value";
+        };
+        in
+        valueType;
+      default = { };
+      example = literalExample { proxy.config.proxy_name = "my_server"; };
+      description = ''
+        List of configurable variables used by Traffic Server.
+
+        Consult the <link xlink:href="${getManualUrl "records.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    remap = mkOption {
+      type = types.lines;
+      default = "";
+      example = "map http://from.example http://origin.example";
+      description = ''
+        URL remapping rules used by Traffic Server.
+
+        Consult the <link xlink:href="${getManualUrl "remap.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    splitDns = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        dest_domain=internal.corp.example named="255.255.255.255:212 255.255.255.254" def_domain=corp.example search_list="corp.example corp1.example"
+        dest_domain=!internal.corp.example named=255.255.255.253
+      '';
+      description = ''
+        Specify the DNS server that Traffic Server should use under specific
+        conditions.
+
+        Consult the <link xlink:href="${getManualUrl "splitdns.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    sslMulticert = mkOption {
+      type = types.lines;
+      default = "";
+      example = "dest_ip=* ssl_cert_name=default.pem";
+      description = ''
+        Configure SSL server certificates to terminate the SSL sessions.
+
+        Consult the <link xlink:href="${getManualUrl "ssl_multicert.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    sni = mkOption {
+      type = types.nullOr yaml.type;
+      default = null;
+      example = literalExample {
+        sni = [{
+          fqdn = "no-http2.example.com";
+          https = "off";
+        }];
+      };
+      description = ''
+        Configure aspects of TLS connection handling for both inbound and
+        outbound connections.
+
+        Consult the <link xlink:href="${getManualUrl "sni.yaml"}">upstream
+        documentation</link> for more details.
+      '';
+    };
+
+    storage = mkOption {
+      type = types.lines;
+      default = "/var/cache/trafficserver 256M";
+      example = "/dev/disk/by-id/XXXXX volume=1";
+      description = ''
+        List all the storage that make up the Traffic Server cache.
+
+        Consult the <link xlink:href="${getManualUrl "storage.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    strategies = mkOption {
+      type = types.nullOr yaml.type;
+      default = null;
+      description = ''
+        Specify the next hop proxies used in an cache hierarchy and the
+        algorithms used to select the next proxy.
+
+        Consult the <link xlink:href="${getManualUrl "strategies.yaml"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+
+    volume = mkOption {
+      type = types.nullOr yaml.type;
+      default = "";
+      example = "volume=1 scheme=http size=20%";
+      description = ''
+        Manage cache space more efficiently and restrict disk usage by
+        creating cache volumes of different sizes.
+
+        Consult the <link xlink:href="${getManualUrl "volume.config"}">
+        upstream documentation</link> for more details.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc = {
+      "trafficserver/cache.config".text = cfg.cache;
+      "trafficserver/hosting.config".text = cfg.hosting;
+      "trafficserver/parent.config".text = cfg.parent;
+      "trafficserver/plugin.config".text = mkPluginConfig cfg.plugins;
+      "trafficserver/records.config".text = mkRecordsConfig cfg.records;
+      "trafficserver/remap.config".text = cfg.remap;
+      "trafficserver/splitdns.config".text = cfg.splitDns;
+      "trafficserver/ssl_multicert.config".text = cfg.sslMulticert;
+      "trafficserver/storage.config".text = cfg.storage;
+      "trafficserver/volume.config".text = cfg.volume;
+    } // (mkYamlConf "ip_allow" cfg.ipAllow)
+    // (mkYamlConf "logging" cfg.logging)
+    // (mkYamlConf "sni" cfg.sni)
+    // (mkYamlConf "strategies" cfg.strategies);
+
+    environment.systemPackages = [ pkgs.trafficserver ];
+    systemd.packages = [ pkgs.trafficserver ];
+
+    # Traffic Server does privilege handling independently of systemd, and
+    # therefore should be started as root
+    systemd.services.trafficserver = {
+      enable = true;
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    # These directories can't be created by systemd because:
+    #
+    #   1. Traffic Servers starts as root and switches to an unprivileged user
+    #      afterwards. The runtime directories defined below are assumed to be
+    #      owned by that user.
+    #   2. The bin/trafficserver script assumes these directories exist.
+    systemd.tmpfiles.rules = [
+      "d '/run/trafficserver' - ${user} ${group} - -"
+      "d '/var/cache/trafficserver' - ${user} ${group} - -"
+      "d '/var/lib/trafficserver' - ${user} ${group} - -"
+      "d '/var/log/trafficserver' - ${user} ${group} - -"
+    ];
+
+    services.trafficserver = {
+      records.proxy.config.admin.user_id = user;
+      records.proxy.config.body_factory.template_sets_dir =
+        "${pkgs.trafficserver}/etc/trafficserver/body_factory";
+    };
+
+    users.users.trafficserver = {
+      description = "Apache Traffic Server";
+      isSystemUser = true;
+      inherit group;
+    };
+    users.groups.trafficserver = { };
+  };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 2347673cef1be..4965cd9228d1a 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -413,6 +413,7 @@ in
   # traefik test relies on docker-containers
   trac = handleTest ./trac.nix {};
   traefik = handleTestOn ["x86_64-linux"] ./traefik.nix {};
+  trafficserver = handleTest ./trafficserver.nix {};
   transmission = handleTest ./transmission.nix {};
   trezord = handleTest ./trezord.nix {};
   trickster = handleTest ./trickster.nix {};
diff --git a/nixos/tests/trafficserver.nix b/nixos/tests/trafficserver.nix
new file mode 100644
index 0000000000000..3979a1b4a4825
--- /dev/null
+++ b/nixos/tests/trafficserver.nix
@@ -0,0 +1,176 @@
+# verifies:
+#   1. Traffic Server is able to start
+#   2. Traffic Server spawns traffic_crashlog upon startup
+#   3. Traffic Server proxies HTTP requests according to URL remapping rules
+#      in 'services.trafficserver.remap'
+#   4. Traffic Server applies per-map settings specified with the conf_remap
+#      plugin
+#   5. Traffic Server caches HTTP responses
+#   6. Traffic Server processes HTTP PUSH requests
+#   7. Traffic Server can load the healthchecks plugin
+#   8. Traffic Server logs HTTP traffic as configured
+#
+# uses:
+#   - bin/traffic_manager
+#   - bin/traffic_server
+#   - bin/traffic_crashlog
+#   - bin/traffic_cache_tool
+#   - bin/traffic_ctl
+#   - bin/traffic_logcat
+#   - bin/traffic_logstats
+#   - bin/tspush
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "trafficserver";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ midchildan ];
+  };
+
+  nodes = {
+    ats = { pkgs, lib, config, ... }: let
+      user = config.users.users.trafficserver.name;
+      group = config.users.groups.trafficserver.name;
+      healthchecks = pkgs.writeText "healthchecks.conf" ''
+        /status /tmp/ats.status text/plain 200 500
+      '';
+    in {
+      services.trafficserver.enable = true;
+
+      services.trafficserver.records = {
+        proxy.config.http.server_ports = "80 80:ipv6";
+        proxy.config.hostdb.host_file.path = "/etc/hosts";
+        proxy.config.log.max_space_mb_headroom = 0;
+        proxy.config.http.push_method_enabled = 1;
+
+        # check that cache storage is usable before accepting traffic
+        proxy.config.http.wait_for_cache = 2;
+      };
+
+      services.trafficserver.plugins = [
+        { path = "healthchecks.so"; arg = toString healthchecks; }
+        { path = "xdebug.so"; }
+      ];
+
+      services.trafficserver.remap = ''
+        map http://httpbin.test http://httpbin
+        map http://pristine-host-hdr.test http://httpbin \
+          @plugin=conf_remap.so \
+          @pparam=proxy.config.url_remap.pristine_host_hdr=1
+        map http://ats/tspush http://httpbin/cache \
+          @plugin=conf_remap.so \
+          @pparam=proxy.config.http.cache.required_headers=0
+      '';
+
+      services.trafficserver.storage = ''
+        /dev/vdb volume=1
+      '';
+
+      networking.firewall.allowedTCPPorts = [ 80 ];
+      virtualisation.emptyDiskImages = [ 256 ];
+      services.udev.extraRules = ''
+        KERNEL=="vdb", OWNER="${user}", GROUP="${group}"
+      '';
+    };
+
+    httpbin = { pkgs, lib, ... }: let
+      python = pkgs.python3.withPackages
+        (ps: with ps; [ httpbin gunicorn gevent ]);
+    in {
+      systemd.services.httpbin = {
+        enable = true;
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${python}/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent";
+        };
+      };
+
+      networking.firewall.allowedTCPPorts = [ 80 ];
+    };
+
+    client = { pkgs, lib, ... }: {
+      environment.systemPackages = with pkgs; [ curl ];
+    };
+  };
+
+  testScript = { nodes, ... }: let
+    sampleFile = pkgs.writeText "sample.txt" ''
+      It's the season of White Album.
+    '';
+  in ''
+    import json
+    import re
+
+    ats.wait_for_unit("trafficserver")
+    ats.wait_for_open_port(80)
+    httpbin.wait_for_unit("httpbin")
+    httpbin.wait_for_open_port(80)
+
+    with subtest("Traffic Server is running"):
+        out = ats.succeed("traffic_ctl server status")
+        assert out.strip() == "Proxy -- on"
+
+    with subtest("traffic_crashlog is running"):
+        ats.succeed("pgrep -f traffic_crashlog")
+
+    with subtest("basic remapping works"):
+        out = client.succeed("curl -vv -H 'Host: httpbin.test' http://ats/headers")
+        assert json.loads(out)["headers"]["Host"] == "httpbin"
+
+    with subtest("conf_remap plugin works"):
+        out = client.succeed(
+            "curl -vv -H 'Host: pristine-host-hdr.test' http://ats/headers"
+        )
+        assert json.loads(out)["headers"]["Host"] == "pristine-host-hdr.test"
+
+    with subtest("caching works"):
+        out = client.succeed(
+            "curl -vv -D - -H 'Host: httpbin.test' -H 'X-Debug: X-Cache' http://ats/cache/60 -o /dev/null"
+        )
+        assert "X-Cache: miss" in out
+
+        out = client.succeed(
+            "curl -vv -D - -H 'Host: httpbin.test' -H 'X-Debug: X-Cache' http://ats/cache/60 -o /dev/null"
+        )
+        assert "X-Cache: hit-fresh" in out
+
+    with subtest("pushing to cache works"):
+        url = "http://ats/tspush"
+
+        ats.succeed(f"echo {url} > /tmp/urls.txt")
+        out = ats.succeed(
+            f"tspush -f '${sampleFile}' -u {url}"
+        )
+        assert "HTTP/1.0 201 Created" in out, "cache push failed"
+
+        out = ats.succeed(
+            "traffic_cache_tool --spans /etc/trafficserver/storage.config find --input /tmp/urls.txt"
+        )
+        assert "Span: /dev/vdb" in out, "cache not stored on disk"
+
+        out = client.succeed(f"curl {url}").strip()
+        expected = (
+            open("${sampleFile}").read().strip()
+        )
+        assert out == expected, "cache content mismatch"
+
+    with subtest("healthcheck plugin works"):
+        out = client.succeed("curl -vv http://ats/status -o /dev/null -w '%{http_code}'")
+        assert out.strip() == "500"
+
+        ats.succeed("touch /tmp/ats.status")
+
+        out = client.succeed("curl -vv http://ats/status -o /dev/null -w '%{http_code}'")
+        assert out.strip() == "200"
+
+    with subtest("logging works"):
+        access_log_path = "/var/log/trafficserver/squid.blog"
+        ats.wait_for_file(access_log_path)
+
+        out = ats.succeed(f"traffic_logcat {access_log_path}").split("\n")[0]
+        expected = "^\S+ \S+ \S+ TCP_MISS/200 \S+ GET http://httpbin/headers - DIRECT/httpbin application/json$"
+        assert re.fullmatch(expected, out) is not None, "no matching logs"
+
+        out = json.loads(ats.succeed(f"traffic_logstats -jf {access_log_path}"))
+        assert out["total"]["error.total"]["req"] == "0", "unexpected log stat"
+  '';
+})
diff --git a/pkgs/development/libraries/cjose/default.nix b/pkgs/development/libraries/cjose/default.nix
new file mode 100644
index 0000000000000..57b5c6c1b8bf5
--- /dev/null
+++ b/pkgs/development/libraries/cjose/default.nix
@@ -0,0 +1,40 @@
+{ lib
+, stdenv
+, fetchFromGitHub
+, autoreconfHook
+, pkg-config
+, doxygen
+, check
+, jansson
+, openssl
+}:
+
+stdenv.mkDerivation rec {
+  pname = "cjose";
+  version = "0.6.1";
+
+  src = fetchFromGitHub {
+    owner = "cisco";
+    repo = "cjose";
+    rev = version;
+    sha256 = "1msyjwmylb5c7jc16ryx3xb9cdwx682ihsm0ni766y6dfwx8bkhp";
+  };
+
+  nativeBuildInputs = [ autoreconfHook pkg-config doxygen ];
+  buildInputs = [ jansson openssl ];
+  checkInputs = [ check ];
+
+  configureFlags = [
+    "--with-jansson=${jansson}"
+    "--with-openssl=${openssl.dev}"
+  ];
+
+  meta = with lib; {
+    homepage = "https://github.com/cisco/cjose";
+    changelog = "https://github.com/cisco/cjose/blob/${version}/CHANGELOG.md";
+    description = "C library for Javascript Object Signing and Encryption";
+    license = licenses.mit;
+    maintainers = with maintainers; [ midchildan ];
+    platforms = platforms.all;
+  };
+}
diff --git a/pkgs/servers/http/trafficserver/default.nix b/pkgs/servers/http/trafficserver/default.nix
new file mode 100644
index 0000000000000..05eb9a17c8e84
--- /dev/null
+++ b/pkgs/servers/http/trafficserver/default.nix
@@ -0,0 +1,207 @@
+{ lib
+, stdenv
+, fetchurl
+, fetchpatch
+, makeWrapper
+, nixosTests
+, pkg-config
+, file
+, linuxHeaders
+, openssl
+, pcre
+, perlPackages
+, python3
+, xz
+, zlib
+# recommended dependencies
+, withHwloc ? true
+, hwloc
+, withCurl ? true
+, curl
+, withCurses ? true
+, ncurses
+, withCap ? stdenv.isLinux
+, libcap
+, withUnwind ? stdenv.isLinux
+, libunwind
+# optional dependencies
+, withBrotli ? false
+, brotli
+, withCjose ? false
+, cjose
+, withGeoIP ? false
+, geoip
+, withHiredis ? false
+, hiredis
+, withImageMagick ? false
+, imagemagick
+, withJansson ? false
+, jansson
+, withKyotoCabinet ? false
+, kyotocabinet
+, withLuaJIT ? false
+, luajit
+, withMaxmindDB ? false
+, libmaxminddb
+# optional features
+, enableWCCP ? false
+}:
+
+stdenv.mkDerivation rec {
+  pname = "trafficserver";
+  version = "9.0.1";
+
+  src = fetchurl {
+    url = "mirror://apache/trafficserver/trafficserver-${version}.tar.bz2";
+    sha256 = "1q164pvfmbqh3gzy3bqy96lwd0fdbhz78r06pd92p7rmkqwx005z";
+  };
+
+  patches = [
+    # Adds support for NixOS
+    # https://github.com/apache/trafficserver/pull/7697
+    (fetchpatch {
+      url = "https://github.com/apache/trafficserver/commit/19d3af481cf74c91fbf713fc9d2f8b138ed5fbaf.diff";
+      sha256 = "0z1ikgpp00rzrrcqh97931586yn9wbksgai9xlkcjd5cg8gq0150";
+    })
+
+    # Fixes a bug in tspush which pushes incorrect contents to cache
+    # https://github.com/apache/trafficserver/pull/7696
+    (fetchpatch {
+      url = "https://github.com/apache/trafficserver/commit/b08215272872f452787915cd3a8e0b0ea0b88385.diff";
+      sha256 = "0axk8x1xvd8wvpgcxgyqqg7kgxyxwfgwmisq3xnk1da0cqv9cx9f";
+    })
+  ];
+
+  # NOTE: The upstream README indicates that flex is needed for some features,
+  # but it actually seems to be unnecessary as of this commit[1]. The detection
+  # logic for bison and flex is still present in the build script[2], but no
+  # other code seems to depend on it. This situation is susceptible to change
+  # though, so it's a good idea to inspect the build scripts periodically.
+  #
+  # [1]: https://github.com/apache/trafficserver/pull/5617
+  # [2]: https://github.com/apache/trafficserver/blob/3fd2c60/configure.ac#L742-L788
+  nativeBuildInputs = [ makeWrapper pkg-config file python3 ]
+    ++ (with perlPackages; [ perl ExtUtilsMakeMaker ])
+    ++ lib.optionals stdenv.isLinux [ linuxHeaders ];
+
+  buildInputs = [
+    openssl
+    pcre
+    perlPackages.perl
+  ] ++ lib.optional withBrotli brotli
+  ++ lib.optional withCap libcap
+  ++ lib.optional withCjose cjose
+  ++ lib.optional withCurl curl
+  ++ lib.optional withGeoIP geoip
+  ++ lib.optional withHiredis hiredis
+  ++ lib.optional withHwloc hwloc
+  ++ lib.optional withImageMagick imagemagick
+  ++ lib.optional withJansson jansson
+  ++ lib.optional withKyotoCabinet kyotocabinet
+  ++ lib.optional withCurses ncurses
+  ++ lib.optional withLuaJIT luajit
+  ++ lib.optional withUnwind libunwind
+  ++ lib.optional withMaxmindDB libmaxminddb;
+
+  outputs = [ "out" "man" ];
+
+  postPatch = ''
+    patchShebangs \
+      iocore/aio/test_AIO.sample \
+      src/traffic_via/test_traffic_via \
+      src/traffic_logstats/tests \
+      tools/check-unused-dependencies
+
+    substituteInPlace configure --replace '/usr/bin/file' '${file}/bin/file'
+  '' + lib.optionalString stdenv.isLinux ''
+    substituteInPlace configure \
+      --replace '/usr/include/linux' '${linuxHeaders}/include/linux'
+  '' + lib.optionalString stdenv.isDarwin ''
+    # 'xcrun leaks' probably requires non-free XCode
+    substituteInPlace iocore/net/test_certlookup.cc \
+      --replace 'xcrun leaks' 'true'
+  '';
+
+  configureFlags = [
+    "--enable-layout=NixOS"
+    "--enable-experimental-plugins"
+    (lib.enableFeature enableWCCP "wccp")
+
+    # the configure script can't auto-locate the following from buildInputs
+    "--with-lzma=${xz.dev}"
+    "--with-zlib=${zlib.dev}"
+    (lib.withFeatureAs withHiredis "hiredis" hiredis)
+  ];
+
+  installFlags = [
+    "pkgsysconfdir=${placeholder "out"}/etc/trafficserver"
+
+    # replace runtime directories with an install-time placeholder directory
+    "pkgcachedir=${placeholder "out"}/.install-trafficserver"
+    "pkglocalstatedir=${placeholder "out"}/.install-trafficserver"
+    "pkglogdir=${placeholder "out"}/.install-trafficserver"
+    "pkgruntimedir=${placeholder "out"}/.install-trafficserver"
+  ];
+
+  postInstall = ''
+    substituteInPlace rc/trafficserver.service --replace "syslog.target" ""
+    install -Dm644 rc/trafficserver.service $out/lib/systemd/system/trafficserver.service
+
+    wrapProgram $out/bin/tspush \
+      --set PERL5LIB '${with perlPackages; makePerlPath [ URI ]}' \
+      --prefix PATH : "${lib.makeBinPath [ file ]}"
+
+    find "$out" -name '*.la' -delete
+
+    # ensure no files actually exist in this directory
+    rmdir $out/.install-trafficserver
+  '';
+
+  installCheckPhase = let
+    expected = ''
+      Via header is [uScMsEf p eC:t cCMp sF], Length is 22
+      Via Header Details:
+      Request headers received from client                   :simple request (not conditional)
+      Result of Traffic Server cache lookup for URL          :miss (a cache "MISS")
+      Response information received from origin server       :error in response
+      Result of document write-to-cache:                     :no cache write performed
+      Proxy operation result                                 :unknown
+      Error codes (if any)                                   :connection to server failed
+      Tunnel info                                            :no tunneling
+      Cache Type                                             :cache
+      Cache Lookup Result                                    :cache miss (url not in cache)
+      Parent proxy connection status                         :no parent proxy or unknown
+      Origin server connection status                        :connection open failed
+    '';
+  in ''
+    runHook preInstallCheck
+    diff -Naur <($out/bin/traffic_via '[uScMsEf p eC:t cCMp sF]') - <<EOF
+    ${lib.removeSuffix "\n" expected}
+    EOF
+    runHook postInstallCheck
+  '';
+
+  doCheck = true;
+  doInstallCheck = true;
+  enableParallelBuilding = true;
+
+  passthru.tests = { inherit (nixosTests) trafficserver; };
+
+  meta = with lib; {
+    homepage = "https://trafficserver.apache.org";
+    changelog = "https://raw.githubusercontent.com/apache/trafficserver/${version}/CHANGELOG-${version}";
+    description = "Fast, scalable, and extensible HTTP caching proxy server";
+    longDescription = ''
+      Apache Traffic Server is a high-performance web proxy cache that improves
+      network efficiency and performance by caching frequently-accessed
+      information at the edge of the network. This brings content physically
+      closer to end users, while enabling faster delivery and reduced bandwidth
+      use. Traffic Server is designed to improve content delivery for
+      enterprises, Internet service providers (ISPs), backbone providers, and
+      large intranets by maximizing existing and available bandwidth.
+    '';
+    license = licenses.asl20;
+    maintainers = with maintainers; [ midchildan joaquinito2051 ];
+    platforms = platforms.unix;
+  };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 012aae2c0b741..92dcd152a1660 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -14039,6 +14039,8 @@ in
 
   cimg = callPackage  ../development/libraries/cimg { };
 
+  cjose = callPackage ../development/libraries/cjose { };
+
   scmccid = callPackage ../development/libraries/scmccid { };
 
   ccrtp = callPackage ../development/libraries/ccrtp { };
@@ -19408,6 +19410,8 @@ in
 
   thanos = callPackage ../servers/monitoring/thanos { };
 
+  trafficserver = callPackage ../servers/http/trafficserver { };
+
   inherit (callPackages ../servers/http/tomcat { })
     tomcat7
     tomcat8