diff --git a/.gitignore b/.gitignore index 2502e81c2..9c74edab2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ /world/ /players/ /resources/ + +# NetherNet server identity key +identity.pem +nethernet_identity.pem diff --git a/config.toml b/config.toml index 9d5cbdd90..714a94756 100644 --- a/config.toml +++ b/config.toml @@ -3,6 +3,19 @@ # The address of the server, including the port. The server will be listening on this address. If another # server is already running on this port, please select a different port. Address = ":19132" + # Transport controls which network transports are listened on. Valid values are raknet, nethernet and both. + Transport = "raknet" + + [Network.NetherNet] + # Address is the TCP address used for NetherNet HTTP signaling. If empty, Network.Address is used. + # The listener serves plaintext HTTP. Terminate HTTPS with a reverse proxy such as Caddy or nginx. + Address = "" + # KeyFile is the path to the PEM file containing the P-384 ECDSA private key used to identify + # this listener when clients connect over plain HTTP. If the file does not exist, a new key is + # generated and saved there. If empty, a temporary key is generated and not saved. + KeyFile = "nethernet_identity.pem" + # Domain is the domain that may be displayed to players connecting over plain HTTP. + Domain = "self" [Server] # The name as it shows up in the server list. Minecraft colour codes may be used in this name to format the diff --git a/go.mod b/go.mod index 381b9dfcd..3de43be96 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,18 @@ go 1.26.0 require ( github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479 github.com/cespare/xxhash/v2 v2.3.0 + github.com/df-mc/go-nethernet v1.0.18 github.com/df-mc/goleveldb v1.1.9 github.com/df-mc/worldupgrader v1.0.21 github.com/go-gl/mathgl v1.2.0 github.com/google/uuid v1.6.0 github.com/pelletier/go-toml v1.9.5 - github.com/sandertv/gophertunnel v1.57.0 + github.com/sandertv/gophertunnel v1.57.1 github.com/segmentio/fasthash v1.0.3 golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 - golang.org/x/mod v0.22.0 - golang.org/x/text v0.23.0 - golang.org/x/tools v0.28.0 + golang.org/x/mod v0.32.0 + golang.org/x/text v0.34.0 + golang.org/x/tools v0.41.0 ) require ( @@ -23,12 +24,33 @@ require ( github.com/df-mc/go-playfab v1.0.0 // indirect github.com/df-mc/go-xsapi v1.0.1 // indirect github.com/df-mc/jsonc v1.0.5 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/klauspost/compress v1.18.4 // indirect + github.com/nxadm/tail v1.4.11 // indirect + github.com/pion/datachannel v1.6.2 // indirect + github.com/pion/dtls/v3 v3.1.4 // indirect + github.com/pion/ice/v4 v4.2.7 // indirect + github.com/pion/interceptor v0.1.45 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns/v2 v2.1.0 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/rtp v1.10.2 // indirect + github.com/pion/sctp v1.10.2 // indirect + github.com/pion/sdp/v3 v3.0.19 // indirect + github.com/pion/srtp/v3 v3.0.12 // indirect + github.com/pion/stun/v3 v3.1.6 // indirect + github.com/pion/transport/v4 v4.0.2 // indirect + github.com/pion/turn/v5 v5.0.10 // indirect + github.com/pion/webrtc/v4 v4.2.16-0.20260627075746-7a223a6f4d4f // indirect github.com/sandertv/go-raknet v1.15.1-0.20260112202637-beca0b10c217 // indirect - golang.org/x/net v0.38.0 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sync v0.12.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/time v0.14.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 4bc90dae7..41fd9c30a 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,10 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/df-mc/go-nethernet v1.0.18 h1:SSlGPhweIux3c9PKhkmyxbMtBc/V2+b9+cJYdyqxbAo= +github.com/df-mc/go-nethernet v1.0.18/go.mod h1:11MfQo5F7PDKfHg1H0zM5mMkrCv29jRE0lK5B4egW9w= github.com/df-mc/go-playfab v1.0.0 h1:6gVukk3aQbJ934GJFdcZJHVIw9lhauK+KHOevbwJA10= github.com/df-mc/go-playfab v1.0.0/go.mod h1:nGOlE+JFGOH5Z0iidEgJapHhndFi/oNk17RN9pKCF+k= github.com/df-mc/go-xsapi v1.0.1 h1:H1SbxYr4rXOqZSB8MwiODbDUsHRihxbHf+YOljUWgXw= @@ -15,10 +19,12 @@ github.com/df-mc/jsonc v1.0.5/go.mod h1:+Q++JuCE9IKiP8v7sWImdf/RjQX0nfXyfX6PdfTT github.com/df-mc/worldupgrader v1.0.21 h1:Qr4/QB8ek7En0vkTuRXYq4FrZM0HHSOXsJOL7Ko4Cjg= github.com/df-mc/worldupgrader v1.0.21/go.mod h1:tsSOLTRm9mpG7VHvYpAjjZrkRHWmSbKZAm9bOLNnlDk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/mathgl v1.2.0 h1:v2eOj/y1B2afDxF6URV1qCYmo1KW08lAMtTbOn3KXCY= github.com/go-gl/mathgl v1.2.0/go.mod h1:pf9+b5J3LFP7iZ4XXaVzZrCle0Q/vNpB/vDe5+3ulRE= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -27,49 +33,100 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pion/datachannel v1.6.2 h1:7EXQ8TH3vTouBUdRWYbcX2edSx9Yj6k5zl5P+qyxEPc= +github.com/pion/datachannel v1.6.2/go.mod h1:pzbdAZvyGtXbcHM1hBbsFaOTf40lZizU/dNlvVOak6E= +github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= +github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= +github.com/pion/ice/v4 v4.2.7 h1:zDEbC6MiEdhQpF8TxBOTws+NU6ZgGpveHrQq4Lc1kao= +github.com/pion/ice/v4 v4.2.7/go.mod h1:9SNPaq0c7El/ki8leJzyCkK10zsskprR3zTNbO3monY= +github.com/pion/interceptor v0.1.45 h1:6PUo/5829bIfRFIPPJQzuDn8EjxRTSB/CSD7QVCOaqo= +github.com/pion/interceptor v0.1.45/go.mod h1:gNDYM/uFKcLe/B3gS2/7+aw6z+RDiMy2qKTnF1LO31w= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY= +github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.10.2 h1:l+f6tTDcAH6xwepaAoW791ddhuYsJlqRATOzirO04Mo= +github.com/pion/rtp v1.10.2/go.mod h1:Au8fc6cEByy8RLTwKTQTEeQqDB/SJDxwL4mZuxYA5Pk= +github.com/pion/sctp v1.10.2 h1:6aezYsMrHAwpjJ6kUdyCiWPqZwgToT00ponT7seJ6a4= +github.com/pion/sctp v1.10.2/go.mod h1:7KFmTwLcoYgJs/Z+99nJvsWL0qDpuyloSI0RbAqlrz0= +github.com/pion/sdp/v3 v3.0.19 h1:1VMKs3gIkTQV5M3hNKfTAPrDXSNrYtOlmOD8+mSZUGQ= +github.com/pion/sdp/v3 v3.0.19/go.mod h1:dE5WOSlzXrtiE/iuZqe9n+AcEbOjtAd3k5m5NtlV/qU= +github.com/pion/srtp/v3 v3.0.12 h1:U7V17bckl7sI4mb3sepiojByDuBY0wNCqQE+6IlQBbc= +github.com/pion/srtp/v3 v3.0.12/go.mod h1:EeZOi/sd6glM1EXapg051gdNWO9yWT1YSsgQ4SlJkns= +github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8= +github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= +github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= +github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= +github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= +github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= +github.com/pion/turn/v5 v5.0.10 h1:mOMZjudflXpte5OsCnXztpUKwNXcpXIAzMBnq9TXOSQ= +github.com/pion/turn/v5 v5.0.10/go.mod h1:u3XjBqy2Z4+NhCUpDoOSsNuQDrPLvKStlCGWk6sTQ1E= +github.com/pion/webrtc/v4 v4.2.16-0.20260627075746-7a223a6f4d4f h1:m39L6AcxVDq1Gwm9/c6QGCiksJ9xoYE4TQyebl9pMu4= +github.com/pion/webrtc/v4 v4.2.16-0.20260627075746-7a223a6f4d4f/go.mod h1:g/C+nTxS7qM2dBr1hRK56OTTD9zFzQDwZi3fMfZxFNM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sandertv/go-raknet v1.15.1-0.20260112202637-beca0b10c217 h1:UZQq2253Q+7co/C9Et62RYPBggzz+L+2yqGlvQhSNM8= github.com/sandertv/go-raknet v1.15.1-0.20260112202637-beca0b10c217/go.mod h1:/yysjwfCXm2+2OY8mBazLzcxJ3irnylKCyG3FLgUPVU= -github.com/sandertv/gophertunnel v1.57.0 h1:UkgVg1xLCsOSm79rP09WmodGSHgA8M7+l4quL01cIL8= -github.com/sandertv/gophertunnel v1.57.0/go.mod h1:W4VnrX9AIPIVXNDMEIKMIRj1T80EdOgdqXpGbQpyAbE= +github.com/sandertv/gophertunnel v1.57.1 h1:P14qngVxDCnSedLvGJtkLrzDOVBQEL1fFliBrT3E9Mc= +github.com/sandertv/gophertunnel v1.57.1/go.mod h1:Frjuk3g1EEGRYjzyrEMS4j/hDvTcez2STfuRHkRPlNU= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/conf.go b/server/conf.go index 16acc6837..a4a8018f0 100644 --- a/server/conf.go +++ b/server/conf.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "slices" + "strings" "time" _ "unsafe" @@ -191,6 +192,7 @@ func (conf Config) New() *Server { l, err := lf(conf) if err != nil { conf.Log.Error("create listener: " + err.Error()) + continue } srv.listeners = append(srv.listeners, l) } @@ -215,6 +217,23 @@ type UserConfig struct { // Address is the address on which the server should listen. Players may // connect to this address in order to join. Address string + // Transport controls which network transports are listened on. Valid values + // are "raknet", "nethernet" and "both". If empty, "raknet" is used. + Transport string + // NetherNet holds settings for the NetherNet HTTP signaling listener. + NetherNet struct { + // Address is the TCP address used for NetherNet HTTP signaling. If empty, + // Network.Address is used. The listener serves plaintext HTTP; HTTPS + // should be terminated by a reverse proxy. + Address string + // KeyFile is the path to the PEM file containing the P-384 ECDSA private + // key used to identify this listener when clients connect over plain HTTP. + // If the file does not exist, a new key is generated and saved there. If + // empty, a temporary key is generated and not saved. + KeyFile string + // Domain is the domain that may be displayed to players connecting over plain HTTP. + Domain string + } } Server struct { // Name is the name of the server as it shows up in the server list. @@ -285,6 +304,10 @@ func (uc UserConfig) Config(log *slog.Logger) (Config, error) { MaxChunkRadius: uc.Players.MaximumChunkRadius, DisableResourceBuilding: !uc.Resources.AutoBuildPack, } + transport, err := parseNetworkTransport(uc.Network.Transport) + if err != nil { + return conf, err + } if !uc.Server.DisableJoinQuitMessages { conf.JoinMessage, conf.QuitMessage = chat.MessageJoin, chat.MessageQuit } @@ -304,10 +327,32 @@ func (uc UserConfig) Config(log *slog.Logger) (Config, error) { return conf, fmt.Errorf("create player provider: %w", err) } } - conf.Listeners = append(conf.Listeners, uc.listenerFunc) + if transport.raknet { + conf.Listeners = append(conf.Listeners, uc.listenerFunc) + } + if transport.nethernet { + conf.Listeners = append(conf.Listeners, uc.netherNetListenerFunc) + } return conf, nil } +type networkTransport struct { + raknet, nethernet bool +} + +func parseNetworkTransport(s string) (networkTransport, error) { + switch strings.ToLower(strings.TrimSpace(s)) { + case "", "raknet": + return networkTransport{raknet: true}, nil + case "nethernet": + return networkTransport{nethernet: true}, nil + case "both": + return networkTransport{raknet: true, nethernet: true}, nil + default: + return networkTransport{}, fmt.Errorf("unknown network transport %q", s) + } +} + // loadResources loads all resource packs found in a directory passed. func loadResources(dir string) ([]*resource.Pack, error) { _ = os.MkdirAll(dir, 0777) @@ -345,6 +390,8 @@ func loadGenerator(dim world.Dimension) world.Generator { func DefaultConfig() UserConfig { c := UserConfig{} c.Network.Address = ":19132" + c.Network.Transport = "raknet" + c.Network.NetherNet.KeyFile, c.Network.NetherNet.Domain = "nethernet_identity.pem", "self" c.Server.Name = "Dragonfly Server" c.Server.AuthEnabled = true c.World.SaveData = true diff --git a/server/listener.go b/server/listener.go index 0c7e46aa2..e39ee809c 100644 --- a/server/listener.go +++ b/server/listener.go @@ -2,11 +2,23 @@ package server import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "errors" "fmt" "io" "log/slog" + "net" + "net/http" + "os" + "time" "github.com/df-mc/dragonfly/server/session" + "github.com/df-mc/go-nethernet" + "github.com/df-mc/go-nethernet/endpoint" "github.com/sandertv/gophertunnel/minecraft" ) @@ -24,6 +36,165 @@ type Listener interface { // listenerFunc may be used to return a *minecraft.Listener using a Config. It // is the standard listener used when UserConfig.Config() is called. func (uc UserConfig) listenerFunc(conf Config) (Listener, error) { + cfg := listenerConfig(conf) + l, err := cfg.Listen("raknet", uc.Network.Address) + if err != nil { + return nil, fmt.Errorf("create minecraft listener: %w", err) + } + conf.Log.Info("Listener running.", "addr", l.Addr()) + return listener{Listener: l}, nil +} + +// importPrivateKey reads a PEM file containing a P-384 [ecdsa.PrivateKey] and +// returns it for use by the NetherNet listener. +func importPrivateKey(path string) (*ecdsa.PrivateKey, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err // already wrapped in os.PathError + } + block, _ := pem.Decode(b) + if block == nil { + return nil, errors.New("invalid PEM block") + } + var key *ecdsa.PrivateKey + switch block.Type { + case "EC PRIVATE KEY": + key, err = x509.ParseECPrivateKey(block.Bytes) + case "PRIVATE KEY": + var parsed any + parsed, err = x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse private key: %w", err) + } + var ok bool + key, ok = parsed.(*ecdsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("must be *ecdsa.PrivateKey: %T", parsed) + } + default: + return nil, fmt.Errorf("invalid block type: %s", block.Type) + } + if err != nil { + return nil, fmt.Errorf("parse private key: %w", err) + } + if key.Curve != elliptic.P384() { + return nil, fmt.Errorf("private key must use P-384, got %s", key.Curve.Params().Name) + } + return key, nil +} + +// exportPrivateKey writes a PEM file containing the [ecdsa.PrivateKey]. +func exportPrivateKey(path string, key *ecdsa.PrivateKey) error { + keyBytes, err := x509.MarshalECPrivateKey(key) + if err != nil { + return fmt.Errorf("encode: %w", err) + } + b := pem.EncodeToMemory(&pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: keyBytes, + }) + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return err + } + if _, err := f.Write(b); err != nil { + _ = f.Close() + _ = os.Remove(path) + return fmt.Errorf("write: %w", err) + } + if err := f.Close(); err != nil { + _ = os.Remove(path) + return fmt.Errorf("close: %w", err) + } + return nil +} + +func (uc UserConfig) netherNetListenerFunc(conf Config) (Listener, error) { + nnConf := uc.Network.NetherNet + address := nnConf.Address + if address == "" { + address = uc.Network.Address + } + if nnConf.Domain == "" { + nnConf.Domain = "self" + } + + lcfg := nethernet.ListenConfig{ + Log: conf.Log.With("net origin", "nethernet"), + AllowAnonymous: conf.AuthDisabled, + } + var key *ecdsa.PrivateKey + if nnConf.KeyFile != "" { + var err error + key, err = importPrivateKey(nnConf.KeyFile) + if os.IsNotExist(err) { + key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate key: %w", err) + } + // If we generated a new key for the NetherNet listener, save it. + // Otherwise, players may be prompted to trust the server identity + // after every restart. + if err := exportPrivateKey(nnConf.KeyFile, key); err != nil { + return nil, fmt.Errorf("export private key: %w", err) + } + lcfg.Log.Info("Generated a private key for NetherNet listener.", "path", nnConf.KeyFile) + } else if err != nil { + return nil, fmt.Errorf("import key file: %w", err) + } + } else { + var err error + key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate key: %w", err) + } + lcfg.Log.Warn("Using a temporary private key for the NetherNet listener. Players connecting over plain HTTP may see the TOFU (Trust On First Use) prompt every time the server restarts.") + } + lcfg.IssueServerIdentity = func(ctx context.Context) (*nethernet.Identity, error) { + return nethernet.GenerateServerIdentity(key, nnConf.Domain) + } + + tcp, err := net.Listen("tcp", address) + if err != nil { + return nil, fmt.Errorf("listen NetherNet HTTP: %w", err) + } + log := conf.Log.With("net origin", "nethernet-http") + handler := endpoint.HandlerConfig{Logger: log}.New() + cfg := listenerConfig(conf) + l, err := cfg.ListenNetwork(minecraft.NetherNet{ + Signaling: handler, + ListenConfig: lcfg, + }, handler.NetworkID()) + if err != nil { + _ = tcp.Close() + return nil, fmt.Errorf("create NetherNet listener: %w", err) + } + + httpServer := &http.Server{ + Addr: address, + Handler: logHTTPRequests(log, handler), + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: 10 * time.Second, + IdleTimeout: 30 * time.Second, + } + go func() { + err := httpServer.Serve(tcp) + if err != nil && !errors.Is(err, http.ErrServerClosed) && !errors.Is(err, net.ErrClosed) { + conf.Log.Error("NetherNet HTTP listener closed unexpectedly: " + err.Error()) + } + }() + conf.Log.Info("NetherNet listener running.", "addr", tcp.Addr()) + return listener{Listener: l, close: httpServer.Close}, nil +} + +func logHTTPRequests(log *slog.Logger, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Info("NetherNet HTTP request.", "method", r.Method, "path", r.URL.Path, "raddr", r.RemoteAddr) + next.ServeHTTP(w, r) + }) +} + +func listenerConfig(conf Config) minecraft.ListenConfig { cfg := minecraft.ListenConfig{ MaximumPlayers: conf.MaxPlayers, StatusProvider: conf.StatusProvider, @@ -35,18 +206,14 @@ func (uc UserConfig) listenerFunc(conf Config) (Listener, error) { if conf.Log.Enabled(context.Background(), slog.LevelDebug) { cfg.ErrorLog = conf.Log.With("net origin", "gophertunnel") } - l, err := cfg.Listen("raknet", uc.Network.Address) - if err != nil { - return nil, fmt.Errorf("create minecraft listener: %w", err) - } - conf.Log.Info("Listener running.", "addr", l.Addr()) - return listener{l}, nil + return cfg } // listener is a Listener implementation that wraps around a minecraft.Listener so that it can be listened on by // Server. type listener struct { *minecraft.Listener + close func() error } // Accept blocks until the next connection is established and returns it. An error is returned if the Listener was @@ -63,3 +230,14 @@ func (l listener) Accept() (session.Conn, error) { func (l listener) Disconnect(conn session.Conn, reason string) error { return l.Listener.Disconnect(conn.(*minecraft.Conn), reason) } + +// Close closes the Minecraft listener and any sidecar listener it depends on. +func (l listener) Close() error { + err := l.Listener.Close() + if l.close != nil { + if closeErr := l.close(); err == nil { + err = closeErr + } + } + return err +}