Skip to content

Commit 63cf583

Browse files
committed
Implement automatic port forwarding
1 parent 9cb4c90 commit 63cf583

File tree

19 files changed

+257
-175
lines changed

19 files changed

+257
-175
lines changed

.idea/gradle.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<!-- Keep a Changelog guide -> https://keepachangelog.com -->
22

3-
# jetbrains-backend-coder Changelog
3+
# coder-gateway-backend changelog
44

55
## [Unreleased]
6-
### Added
7-
- Initial scaffold created from [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template)

README.md

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,17 @@
1-
# jetbrains-backend-coder
1+
# coder-gateway-backend
22

3-
![Build](https://github.com/code-asher/jetbrains-backend-coder/workflows/Build/badge.svg)
4-
[![Version](https://img.shields.io/jetbrains/plugin/v/PLUGIN_ID.svg)](https://plugins.jetbrains.com/plugin/PLUGIN_ID)
5-
[![Downloads](https://img.shields.io/jetbrains/plugin/d/PLUGIN_ID.svg)](https://plugins.jetbrains.com/plugin/PLUGIN_ID)
6-
7-
## Template ToDo list
8-
- [x] Create a new [IntelliJ Platform Plugin Template][template] project.
9-
- [ ] Get familiar with the [template documentation][template].
10-
- [ ] Adjust the [pluginGroup](./gradle.properties), [plugin ID](./src/main/resources/META-INF/plugin.xml) and [sources package](./src/main/kotlin).
11-
- [ ] Adjust the plugin description in `README` (see [Tips][docs:plugin-description])
12-
- [ ] Review the [Legal Agreements](https://plugins.jetbrains.com/docs/marketplace/legal-agreements.html?from=IJPluginTemplate).
13-
- [ ] [Publish a plugin manually](https://plugins.jetbrains.com/docs/intellij/publishing-plugin.html?from=IJPluginTemplate) for the first time.
14-
- [ ] Set the `PLUGIN_ID` in the above README badges.
15-
- [ ] Set the [Plugin Signing](https://plugins.jetbrains.com/docs/intellij/plugin-signing.html?from=IJPluginTemplate) related [secrets](https://github.com/JetBrains/intellij-platform-plugin-template#environment-variables).
16-
- [ ] Set the [Deployment Token](https://plugins.jetbrains.com/docs/marketplace/plugin-upload.html?from=IJPluginTemplate).
17-
- [ ] Click the <kbd>Watch</kbd> button on the top of the [IntelliJ Platform Plugin Template][template] to be notified about releases containing new features and fixes.
3+
![Build](https://github.com/coder/jetbrains-backend-coder/workflows/Build/badge.svg)
4+
[![Version](https://img.shields.io/jetbrains/plugin/v/coder-gateway-backend.svg)](https://plugins.jetbrains.com/plugin/coder-gateway-backend)
5+
[![Downloads](https://img.shields.io/jetbrains/plugin/d/coder-gateway-backend.svg)](https://plugins.jetbrains.com/plugin/coder-gateway-backend)
186

197
<!-- Plugin description -->
20-
This Fancy IntelliJ Platform Plugin is going to be your implementation of the brilliant ideas that you have.
21-
22-
This specific section is a source for the [plugin.xml](/src/main/resources/META-INF/plugin.xml) file which will be extracted by the [Gradle](/build.gradle.kts) during the build process.
23-
24-
To keep everything working, do not remove `<!-- ... -->` sections.
8+
This plugin is meant to be installed on a remote machine and used as a companion
9+
to using Coder through Gateway.
2510
<!-- Plugin description end -->
2611

2712
## Installation
2813

29-
- Using the IDE built-in plugin system:
30-
31-
<kbd>Settings/Preferences</kbd> > <kbd>Plugins</kbd> > <kbd>Marketplace</kbd> > <kbd>Search for "jetbrains-backend-coder"</kbd> >
32-
<kbd>Install</kbd>
33-
3414
- Manually:
3515

36-
Download the [latest release](https://github.com/code-asher/jetbrains-backend-coder/releases/latest) and install it manually using
16+
Download the [latest release](https://github.com/coder/jetbrains-backend-coder/releases/latest) and install it manually using
3717
<kbd>Settings/Preferences</kbd> > <kbd>Plugins</kbd> > <kbd>⚙️</kbd> > <kbd>Install plugin from disk...</kbd>
38-
39-
40-
---
41-
Plugin based on the [IntelliJ Platform Plugin Template][template].
42-
43-
[template]: https://github.com/JetBrains/intellij-platform-plugin-template
44-
[docs:plugin-description]: https://plugins.jetbrains.com/docs/intellij/plugin-user-experience.html#plugin-description-and-presentation

gradle.properties

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html
22

3-
pluginGroup = com.github.codeasher.jetbrainsbackendcoder
4-
pluginName = jetbrains-backend-coder
5-
pluginRepositoryUrl = https://github.com/code-asher/jetbrains-backend-coder
3+
pluginGroup = com.coder.jetbrains
4+
pluginName = coder-gateway-backend
5+
pluginRepositoryUrl = https://github.com/coder/jetbrains-backend-coder
66
# SemVer format -> https://semver.org
77
pluginVersion = 0.0.1
88

@@ -16,7 +16,7 @@ platformVersion = 2022.3.3
1616

1717
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
1818
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
19-
platformPlugins =
19+
platformPlugins=com.jetbrains.codeWithMe, org.jetbrains.plugins.terminal
2020

2121
# Gradle Releases -> https://github.com/gradle/gradle/releases
2222
gradleVersion = 8.6

src/main/kotlin/com/github/codeasher/jetbrainsbackendcoder/MyBundle.kt renamed to src/main/kotlin/com/coder/jetbrains/CoderBundle.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
package com.github.codeasher.jetbrainsbackendcoder
1+
package com.coder.jetbrains
22

33
import com.intellij.DynamicBundle
44
import org.jetbrains.annotations.NonNls
55
import org.jetbrains.annotations.PropertyKey
66

77
@NonNls
8-
private const val BUNDLE = "messages.MyBundle"
8+
private const val BUNDLE = "messages.CoderBundle"
99

10-
object MyBundle : DynamicBundle(BUNDLE) {
10+
object CoderBundle : DynamicBundle(BUNDLE) {
1111

1212
@JvmStatic
1313
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) =
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.coder.jetbrains.scanner
2+
3+
import java.io.File
4+
5+
private val sources = listOf(
6+
"/proc/net/tcp",
7+
"/proc/net/tcp6"
8+
)
9+
10+
private val whitespaceRe = "\\s+".toRegex()
11+
12+
/**
13+
* Parse a TCP socket /proc interface.
14+
*/
15+
fun readTcpFile(file: File): Set<Int> {
16+
/*
17+
* A typical entry of /proc/net/tcp would look like this (only the first
18+
* four parts since that is all we use):
19+
*
20+
* 46: 010310AC:9C4C 030310AC:1770 01
21+
* | | | | | |--> connection state
22+
* | | | | |------> remote TCP port number
23+
* | | | |-------------> remote IPv4 address
24+
* | | |--------------------> local TCP port number
25+
* | |---------------------------> local IPv4 address
26+
* |----------------------------------> number of entry
27+
*
28+
* connection states:
29+
* TCP_ESTABLISHED,
30+
* TCP_SYN_SENT,
31+
* TCP_SYN_RECV,
32+
* TCP_FIN_WAIT1,
33+
* TCP_FIN_WAIT2,
34+
* TCP_TIME_WAIT,
35+
* TCP_CLOSE,
36+
* TCP_CLOSE_WAIT,
37+
* TCP_LAST_ACK,
38+
* TCP_LISTEN,
39+
* TCP_CLOSING,
40+
* TCP_NEW_SYN_RECV,
41+
*/
42+
return try {
43+
file.readLines()
44+
.asSequence()
45+
// Fields are separated by spaces.
46+
.map { it.trim().split(whitespaceRe) }
47+
// Only TCP_LISTEN.
48+
.filter { it.size >= 4 && it[3] == "0A" }
49+
// Local address.
50+
.map { it[1].split(":") }
51+
.filter { it.size == 2 }
52+
// Decode port from hex.
53+
.map { it[1].toIntOrNull(16) }
54+
.filterNotNull()
55+
.toSet()
56+
} catch (ex: Exception) {
57+
// TODO: Log this exception?
58+
emptySet()
59+
}
60+
}
61+
62+
/**
63+
* Return a list of ports with listening TCP sockets. It does not differentiate
64+
* between ipv4 and ipv6, and it does not return addresses since the forwarding
65+
* API does not appear to let us give it an address anyway.
66+
*
67+
* It only supports Linux, as that is the only place remote IDEs can run anyway.
68+
*/
69+
fun listeningPorts(): Set<Int> {
70+
// REVIEW: We could instead query /workspaceagents/me/listening-ports (after
71+
// adding it to coderd) which would let us share the port scanning code with
72+
// the agent?
73+
return sources.flatMap { readTcpFile(File(it)) }.toSet()
74+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.coder.jetbrains.services
2+
3+
import com.coder.jetbrains.scanner.listeningPorts
4+
import com.intellij.openapi.Disposable
5+
import com.intellij.openapi.components.service
6+
import com.intellij.openapi.diagnostic.thisLogger
7+
import com.intellij.util.application
8+
import com.jetbrains.rd.platform.codeWithMe.portForwarding.ClientPortAttributes
9+
import com.jetbrains.rd.platform.codeWithMe.portForwarding.ClientPortPickingStrategy
10+
import com.jetbrains.rd.platform.codeWithMe.portForwarding.PerClientPortForwardingManager
11+
import com.jetbrains.rd.platform.codeWithMe.portForwarding.PortAlreadyForwardedException
12+
import com.jetbrains.rd.platform.codeWithMe.portForwarding.PortType
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.Job
15+
import kotlinx.coroutines.delay
16+
import kotlinx.coroutines.isActive
17+
import kotlinx.coroutines.launch
18+
19+
/**
20+
* Automatically forward ports that have something listening on them by scanning
21+
* /proc/net/ipv{,6} at a regular interval.
22+
*
23+
* If a process stops listening the port forward is removed.
24+
*
25+
* If the user manually removes a port, it will be added back at the next
26+
* interval.
27+
*/
28+
@Suppress("UnstableApiUsage")
29+
class CoderPortForwardService(
30+
private val cs: CoroutineScope,
31+
): Disposable {
32+
private var poller: Job? = null
33+
34+
// TODO: Make customizable.
35+
private val ignoreList = setOf(
36+
22, // SSH
37+
5990, // Default JetBrains remote port.
38+
)
39+
40+
init {
41+
thisLogger().info("initializing port forwarding service")
42+
application.invokeLater {
43+
start()
44+
}
45+
}
46+
47+
override fun dispose() {
48+
poller?.cancel()
49+
}
50+
51+
private fun start() {
52+
thisLogger().info("starting port scanner")
53+
poller = cs.launch {
54+
while (isActive) {
55+
thisLogger().debug("scanning for ports")
56+
val listeningPorts = listeningPorts().subtract(ignoreList)
57+
val manager = service<PerClientPortForwardingManager>()
58+
val ports = manager.getPorts()
59+
// Remove ports that are no longer listening.
60+
// TODO: Only remove ones we added?
61+
ports.forEach { old ->
62+
if (!listeningPorts.contains(old.hostPortNumber)) {
63+
try {
64+
thisLogger().info("removing port ${old.hostPortNumber}")
65+
manager.removePort(old)
66+
} catch (ex: Exception) {
67+
thisLogger().error("failed to remove port $old", ex)
68+
}
69+
}
70+
}
71+
// Add ports that are not yet listening.
72+
// TODO: Avoid adding if the user removed it previously?
73+
listeningPorts.forEach {
74+
try {
75+
thisLogger().info("forwarding port $it")
76+
forwardPort(it)
77+
} catch (ex: PortAlreadyForwardedException) {
78+
// All good.
79+
} catch (ex: Exception) {
80+
thisLogger().error("failed to forward port $it", ex)
81+
}
82+
}
83+
// TODO: Customizable interval.
84+
delay(5000)
85+
}
86+
}
87+
}
88+
89+
/**
90+
* Add a port forward to the provided port on the host.
91+
*
92+
* TODO: If privileged, use a different port.
93+
*/
94+
private fun forwardPort(port: Int) {
95+
val manager = service<PerClientPortForwardingManager>()
96+
manager.forwardPort(port, PortType.TCP, setOf("coder"), ClientPortAttributes(
97+
preferredPortNumber = port,
98+
strategy = ClientPortPickingStrategy.REASSIGN_WHEN_BUSY,
99+
))
100+
}
101+
}

src/main/kotlin/com/github/codeasher/jetbrainsbackendcoder/listeners/MyApplicationActivationListener.kt

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/main/kotlin/com/github/codeasher/jetbrainsbackendcoder/services/MyProjectService.kt

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/main/kotlin/com/github/codeasher/jetbrainsbackendcoder/toolWindow/MyToolWindowFactory.kt

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
22
<idea-plugin>
3-
<id>com.github.codeasher.jetbrainsbackendcoder</id>
4-
<name>jetbrains-backend-coder</name>
5-
<vendor>code-asher</vendor>
3+
<id>com.coder.jetbrains</id>
4+
<name>Coder Gateway Backend</name>
5+
<vendor>coder</vendor>
66

77
<depends>com.intellij.modules.platform</depends>
8+
<dependencies>
9+
<plugin id="com.jetbrains.codeWithMe"/>
10+
</dependencies>
811

9-
<resource-bundle>messages.MyBundle</resource-bundle>
12+
<resource-bundle>messages.CoderBundle</resource-bundle>
1013

1114
<extensions defaultExtensionNs="com.intellij">
12-
<toolWindow factoryClass="com.github.codeasher.jetbrainsbackendcoder.toolWindow.MyToolWindowFactory" id="MyToolWindow"/>
15+
<applicationService serviceImplementation="com.coder.jetbrains.services.CoderPortForwardService"
16+
client="controller" preload="true"/>
1317
</extensions>
14-
15-
<applicationListeners>
16-
<listener class="com.github.codeasher.jetbrainsbackendcoder.listeners.MyApplicationActivationListener" topic="com.intellij.openapi.application.ApplicationActivationListener"/>
17-
</applicationListeners>
1818
</idea-plugin>

src/main/resources/messages/CoderBundle.properties

Whitespace-only changes.

src/main/resources/messages/MyBundle.properties

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)