{"id":49281760,"url":"https://github.com/pmuller/udp46","last_synced_at":"2026-04-25T19:00:45.423Z","repository":{"id":353815142,"uuid":"1221016653","full_name":"pmuller/udp46","owner":"pmuller","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-25T17:00:10.000Z","size":24,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-25T18:30:52.835Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pmuller.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-25T16:36:27.000Z","updated_at":"2026-04-25T16:58:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pmuller/udp46","commit_stats":null,"previous_names":["pmuller/udp46"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/pmuller/udp46","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmuller%2Fudp46","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmuller%2Fudp46/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmuller%2Fudp46/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmuller%2Fudp46/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pmuller","download_url":"https://codeload.github.com/pmuller/udp46/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pmuller%2Fudp46/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32273223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"ssl_error","status_checked_at":"2026-04-25T18:29:32.149Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-25T19:00:44.699Z","updated_at":"2026-04-25T19:00:45.417Z","avatar_url":"https://github.com/pmuller.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# udp46\n\n`udp46` is a small stateful UDP relay for exposing IPv6-only UDP services to IPv4-only clients. It listens on one or more IPv4 UDP sockets and forwards datagrams to one IPv6 upstream without parsing or terminating the application protocol.\n\nThe primary use case is an IPv4-only mobile client reaching a home WireGuard endpoint that is reachable only over IPv6:\n\n```text\nIPv4-only client\n  -\u003e dual-stack VPS IPv4 UDP listener\n  -\u003e IPv6-only home WireGuard endpoint\n```\n\nThe relay is payload-blind. It does not hold WireGuard keys, decrypt traffic, authenticate peers, inspect QUIC, or understand any UDP protocol.\n\n`udp46` is designed for the common dual-stack VPS case where lower-layer\nIPv4/IPv6 translation is not practical: you have one normal public IPv4 address\nand one normal public IPv6 address, but no routed IPv6 translation prefix.\nUnlike SIIT/NAT46, `udp46` does not need a routed `/96` or proxy NDP. Unlike\nmany generic UDP proxies, it creates one upstream IPv6 socket per client session\nso the upstream sees distinct source ports.\n\n`udp46` is licensed under the MIT License. See [LICENSE](LICENSE).\n\n## How It Works\n\nFor every unique tuple of client IPv4 address, client UDP port, and listener identity, `udp46` creates an in-memory session with a dedicated connected IPv6 UDP socket to the upstream.\n\n```text\n192.0.2.10:62000 -\u003e udp46 0.0.0.0:51820\nudp46 creates [2001:db8::5]:41001 -\u003e [2001:db8:100::10]:51820\n\n198.51.100.20:62000 -\u003e udp46 0.0.0.0:51820\nudp46 creates [2001:db8::5]:41002 -\u003e [2001:db8:100::10]:51820\n```\n\nThe upstream sees distinct source ports, which lets protocols such as WireGuard maintain separate peer endpoints.\n\nThis is transport plumbing only. Application-level routing, policy, and\nauthentication remain the responsibility of the upstream service.\n\n## Install And Run\n\nBuild locally:\n\n```sh\ngo build -o udp46 ./cmd/udp46\n```\n\nRun in the foreground:\n\n```sh\n./udp46 \\\n  --listen 0.0.0.0:51820 \\\n  --listen 0.0.0.0:443 \\\n  --upstream '[2001:db8:100::10]:51820' \\\n  --session-timeout 180s\n```\n\nWith metrics and debug HTTP enabled:\n\n```sh\n./udp46 \\\n  --listen 0.0.0.0:51820 \\\n  --upstream '[2001:db8:100::10]:51820' \\\n  --metrics.enabled \\\n  --metrics.listen 127.0.0.1:9108\n```\n\nConfiguration is currently CLI-only. Upstream DNS names are resolved at startup; dynamic DNS changes require a restart.\n\n## Flags\n\n- `--listen`: IPv4 UDP listen address. Repeat for multiple listeners.\n- `--upstream`: IPv6 UDP upstream address or hostname and port.\n- `--session-timeout`: idle session timeout. Default: `180s`.\n- `--read-buffer-size`: UDP read buffer size. Default: `65536`.\n- `--write-timeout`: UDP write timeout. Default: `5s`.\n- `--log-level`: `debug`, `info`, `warn`, or `error`. Default: `info`.\n- `--metrics.enabled`: enable `/metrics` and `/debug/sessions`. Default: disabled.\n- `--metrics.listen`: metrics HTTP address. Default: `127.0.0.1:9108`.\n- `--metrics.session-labels`: expose high-cardinality per-session Prometheus labels.\n- `--max-sessions`: maximum active sessions. `0` means unlimited.\n- `--version`: print embedded version metadata.\n\n## WireGuard Example\n\nThe mobile client points at the VPS:\n\n```ini\n[Peer]\nPublicKey = \u003cserver-public-key\u003e\nEndpoint = vps.example.net:51820\nPersistentKeepalive = 25\nAllowedIPs = 0.0.0.0/0, ::/0\n```\n\nThe home WireGuard server remains on the IPv6-only home firewall, for example `[2001:db8:100::10]:51820`.\n\n`AllowedIPs` controls routing inside the WireGuard tunnel. `udp46` only affects\nthe outer UDP transport between the IPv4-only client and the IPv6-only endpoint.\n\nSee [docs/wireguard.md](docs/wireguard.md).\n\n## Session Persistence\n\nSessions are in-memory only. Restarting `udp46` drops all mappings.\n\nFor WireGuard, this causes a short interruption. Active clients recover on the next outbound packet. Idle clients recover on the next `PersistentKeepalive = 25` packet, after which WireGuard authenticates the packet and updates the peer endpoint.\n\nPersistent session storage is a non-goal for v1.\n\n## Metrics And Debug\n\nMetrics are disabled by default. When enabled, `/metrics` exposes low-cardinality Prometheus metrics for listeners, sessions, datagrams, bytes, drops, errors, and session lifetime histograms.\n\n`/debug/sessions` exposes JSON session details:\n\n- listener address\n- client IPv4 address and UDP port\n- upstream local IPv6 address and UDP port\n- upstream remote IPv6 address and UDP port\n- created and last-activity timestamps\n- packet and byte counters in both directions\n\nPer-session Prometheus labels are disabled by default because they are high-cardinality. Enable `--metrics.session-labels` only for small deployments or temporary debugging.\n\n## Security Model\n\n`udp46` is payload-blind and does not log packet payloads. It is still an internet-facing UDP relay, so host firewalling matters.\n\nRecommended production controls:\n\n- bind only required IPv4 UDP ports;\n- restrict sources with nftables when possible;\n- expose metrics/debug only on loopback or a private management network;\n- run as a dedicated unprivileged user;\n- use systemd hardening.\n\nExample nftables allowlist:\n\n```nft\ntable inet filter {\n  chain input {\n    type filter hook input priority 0;\n    policy drop;\n\n    iif lo accept\n    ct state established,related accept\n    ip saddr { 192.0.2.0/24, 198.51.100.0/24 } udp dport 51820 accept\n  }\n}\n```\n\nExample systemd unit:\n\n```ini\n[Unit]\nDescription=udp46 IPv4-to-IPv6 UDP relay\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nUser=udp46\nGroup=udp46\nExecStart=/usr/local/bin/udp46 --listen 0.0.0.0:51820 --upstream [2001:db8:100::10]:51820\nRestart=always\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\nProtectHome=true\nRestrictAddressFamilies=AF_INET AF_INET6\nAmbientCapabilities=CAP_NET_BIND_SERVICE\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\n\n[Install]\nWantedBy=multi-user.target\n```\n\n## Non-Goals\n\n`udp46` is not a VPN, NAT46/SIIT implementation, firewall, TLS or WireGuard terminator, TCP proxy, anonymity tool, or protocol parser. It does not implement persistent session state.\n\n## Comparison\n\nJool and SIIT solve IP translation at a lower layer and are the right tools when\nyou have the routing primitives they need. Stateless SIIT/NAT46 typically needs\na routed IPv6 translation prefix, such as a `/96`, so translated source\naddresses can route back to the translator. `udp46` is intentionally narrower:\nit relays UDP datagrams between IPv4 clients and one IPv6 upstream while\npreserving one upstream source port per client session.\n\nGeneric UDP proxies can forward packets, but many use one shared upstream socket. That breaks protocols such as WireGuard because multiple clients appear to the upstream as the same source endpoint. `udp46` creates a dedicated upstream socket per client tuple.\n\n`socat` is useful for quick connectivity experiments, but it is not the target\noperational model. `udp46` makes the session table explicit, exports per-client\nstate and counters, applies idle expiry and capacity limits deliberately, and\nlogs structured metadata without packet payloads. Those controls are what make\nthe relay debuggable as long-running infrastructure rather than a one-off\npacket plumbing command.\n\n## Development\n\nRun checks:\n\n```sh\ngo test ./...\ngo test -race ./...\ngo vet ./...\ngofmt -w cmd internal\n```\n\nCI runs tests, race tests, vet, formatting checks, and a binary build.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpmuller%2Fudp46","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpmuller%2Fudp46","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpmuller%2Fudp46/lists"}